Adding a Web App
A new Flask app joins tools.npa.school by being mounted under its own URL prefix in the one PythonAnywhere web app that serves the site. The existing apps keep running; the new one gets its own directory, its own config file, and its own MySQL database, and shares only the Python process and the virtualenv. How that arrangement works, and the conventions behind every rule below, are on Python Server Setup. This page is the routine.
Pick the app's name before you start. Everything else is derived from it.
| Item | Pattern | Example (print queue) |
|---|---|---|
| Directory | ~/<name>-app |
~/printqueue-app |
| Database | misterwillis$<name> |
misterwillis$printqueue |
| Mount path | /<short-name> |
/3dp |
| OAuth callback | https://tools.npa.school/<mount>/auth/callback |
https://tools.npa.school/3dp/auth/callback |
Mount Paths Are Permanent
Keep the mount path short, and never change it once the app has users. The Google OAuth redirect URI embeds it, so a renamed mount breaks sign-in for everyone until the URI list is fixed.
What the App Needs
Every app keeps its secrets in its own config file (a .env or .toml inside
the app directory), never in the WSGI file and never in the zip that deploys the
code. This table lists what a typical app needs and where each value comes from.
The values themselves are not written down anywhere on this wiki.
| Secret | Source | Notes |
|---|---|---|
| MySQL password | Databases tab (set once, account-wide) | Same password for every misterwillis$... database |
| Google Client ID | Google Cloud console ▸ APIs & Services ▸ Credentials | One shared OAuth client is reused by all apps; the ID is not sensitive, but it lives with the secret |
| Google Client Secret | Same credentials page | Treat it like a password |
Flask secret_key |
Generate per app: python3 -c "import secrets; print(secrets.token_hex(32))" |
Signs session cookies; a new one logs everyone out |
| App-specific tokens | Generate the same way | For example, the print queue's agent_token for its Raspberry Pi API client |
Mount the App
Steps are numbered continuously through this page so any step can be referred to by its number.
-
Name it. Choose
<name>. That fixes the directory~/<name>-app, the databasemisterwillis$<name>, and the mount path/<short-name>, per the table at the top of this page. -
Upload the code. Zip the project locally (exclude
__pycache__), upload the zip via the Files tab, then unpack it in a Bash console: -
Install its dependencies into the shared venv.
or, if the project ships a requirements file,
Adding packages is safe for the other apps. Upgrading a package the apps already share (Flask, for example) affects all of them, so check the release notes before letting
pipdo it. -
Create its database on the Databases tab:
misterwillis$<name>. Apps should create their own tables on first start (CREATE TABLE IF NOT EXISTS). If this one doesn't, run its schema manually from a console. -
Write its config file inside
~/<name>-app, holding the secrets from the table above. Use absolute paths in the config; the WSGI process's working directory is not the app directory, so a relative path points somewhere you didn't intend. -
Add a WSGI section. Open
/var/www/tools_npa_school_wsgi.pyfrom the Web tab, copy an existing app's section, point the copy at the new directory, import the app's factory, and add the app toMOUNTSwith its mount path. The singleDispatcherMiddlewarecall at the bottom of the file picks it up from there.The App Thinks It Lives at /
The mounted app sees its own URLs from
/. Werkzeug setsSCRIPT_NAMEfor the mount, sourl_forand cookies come out right with no changes to the app's code.No Secrets in the WSGI File
The WSGI file holds only paths and imports. Secrets belong in the config file from step 5.
-
Register the OAuth redirect URI, if the app offers Google sign-in. All apps share one OAuth client (an External client; each app filters which Google accounts it accepts), so this is a matter of adding one URI to it:
- In the Google Cloud console, open APIs & Services ▸ Credentials.
- Open the existing OAuth 2.0 client.
- Under Authorized redirect URIs, add the app's callback, which includes
its mount path:
https://tools.npa.school/<mount>/auth/callback(for example/3dp/auth/callbackor/queue/auth/callback). - Save. The change takes effect within a few minutes, and nothing on PythonAnywhere needs a reload for this step.
The client ID and secret stay the same for every app, so no IT re-approval is needed. The approval attaches to the client, not to the URI list.
-
Reload on the Web tab, then smoke-test in this order: the existing apps first, then the new app's pages. The apps share one process, so a syntax error in the new WSGI section takes everything down, and the existing apps are the quickest way to find out.
Done
The app is live at https://tools.npa.school/<mount>. Add it to the mounts
table on Python Server Setup so the
record stays current.
If It Misbehaves
- Site-wide 500 after the reload. Open the Web tab error log and read the bottom of the file. The WSGI section you just added is the usual suspect; all apps fail together because one section failed at import time.
- Take the new app offline without touching the others: delete (or comment out) its WSGI section and Reload. Its directory, config, and database are untouched, and it can be re-mounted later once the problem is fixed.
- Suspect the venv? Blanking the Web tab's Virtualenv field falls back to system Python (only system-wide packages will be available), which is a quick way to bisect whether the venv is the problem.
Updating the App Later
Updates follow the same path as the first upload, minus the setup: zip the
project locally, upload via the Files tab, unzip over ~/<name>-app in a
console (the step 2 command works unchanged), and Reload. Config and data live
outside the zip, so an update never touches them.
Habits Worth Keeping
- Open one MySQL connection per request and close it on teardown.
PythonAnywhere enforces
max_user_connections, and both current apps follow this rule. - Keep any upload limit at or below about 100 MB, the platform's request body cap.
- For cron-style work (nightly cleanup, for example), use Scheduled Tasks
on the Tasks tab and call the venv's interpreter explicitly,
~/.virtualenvs/tools-venv/bin/python <script>, since tasks run with system Python by default. Tasks, consoles, and the web app share the home directory, so the script can read the app's own config file. - Remember that the database counts toward the account's storage quota.