Setup guide
Everything you need to run Instrumentify for one school. If you would rather not run a server, email instead and it gets set up for you.
Install
Two ways. The container is the one to use on a school server, a TrueNAS box or a VPS. The pip install is for trying it on a laptop or developing.
With Docker
docker run -d --name instrumentify -p 5000:5000 \
-v /srv/instrumentify/data:/app/data \
-v /srv/instrumentify/logs:/app/logs \
-v /srv/instrumentify/uploads:/app/static/uploads \
-v /srv/instrumentify/config:/app/config \
-v /srv/instrumentify/google_ServiceAccount:/app/google_ServiceAccount \
ghcr.io/itsrecharge/instrumentify:latest
The five folders hold everything that must survive an image update: the CSV data, logs, instrument photos, your configuration, and the Google service-account file if you use one. On TrueNAS SCALE, use repository ghcr.io/itsrecharge/instrumentify, container port 5000, and mount the same five paths.
With Python
git clone https://github.com/ItsRecharge/Instrumentify
cd Instrumentify
python3.11 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
cp -r config_example config
python app.py # serves on port 5001
For production, run it under gunicorn with one worker and put HTTPS in front of it (Caddy, nginx or the reverse proxy your school already has):
gunicorn -w 1 -b 0.0.0.0:5000 wsgi:app
Setup wizard
Open the app, sign in with the example admin from config/config.py, and Instrumentify walks you through five steps. It writes the configuration for you; there is no file editing.
School and admin account
The name shown everywhere, a fresh secret key, and the real director account. The example address
admin@example.comis refused on purpose.School year and loan terms
When your year starts and ends. Due dates for school-year, summer and both-term loans are computed from this, and the renewal window opens before the summer.
Google Sheets and email
Optional. Upload the service-account file, paste the spreadsheet ID and worksheet names, add a Gmail app password. Both can be skipped and turned on later.
Public pages
Whether families see a landing page, a catalog and a features page, and what the hero says. Each is a switch you can flip later.
Review and finish
A summary of every choice. Finishing signs you in as the new admin and unlocks the rest of the app.
Day-to-day use
- Add instruments one at a time with photos, or bulk-set school levels from the list. Each gets a UUID you can print on a label and scan into the search box.
- Checkouts are for the school year, the summer or both. A manual checkout takes a student's name, email, ID and grad year. Checking in records the condition and any notes.
- Requests from the Google Form appear in the queue with the student's photos. Approving one checks the instrument out and emails the student; denying one emails them too.
- Renewals let a student keep an instrument over the summer. They can be auto-approved inside the renewal window.
- Other staff get accounts by redeeming an invite code you generate from the account menu. There is no open sign-up.
- Exports of instruments, users and history are one click each, as CSV.
Google Forms, Sheets and Drive
All optional, all separate switches. The core inventory and manual checkouts work with no external services at all.
Requests through a Google Form
Students fill in a form with the instrument UUID, their name, email, student ID, graduation year, the loan term, and a few photos of the instrument's condition. Uploads land in the form owner's Drive and the links flow into the linked spreadsheet. Instrumentify reads that sheet every 30 seconds, so the queue stays current, and writes the outcome back when a request is approved or denied.
Backups to Google Sheets
Every edit mirrors the CSV files to a spreadsheet the school owns. If the server dies, the data is readable in Sheets and can be restored from it with one button.
Photo storage
Photos uploaded in the app stay on the server. Photos that arrive through the form live in Drive. The storage manager watches the Drive quota: recent images stay full size in Drive, older ones are downloaded and compressed locally, and past 90% usage it clears old cloud copies while keeping anything younger than six months. iPhone HEIC photos are handled.
Notifications use a Gmail app password over SMTP. Students get approval, denial and reminder emails; the director gets either one email per request or a daily summary. Many school Google accounts block SMTP, in which case a standalone Gmail works.
How it is put together
app.py Flask entrypoint
app/
models/ user, instrument, checkout (CSV-backed)
routes/ auth, admin, api, checkout, instruments, setup
services/ email, google_sheets, audit, storage_manager
utils/ security, helpers, file locking
config/ your settings (never committed)
config_example/ template the wizard fills in
data/ CSV files, the source of truth
static/uploads/ instrument photos
templates/ Jinja2 pages
| Config module | What it holds |
|---|---|
config.py | Secret key, admin credentials, school name, debug flag |
config_server.py | Upload paths, CSV locations, invite-code expiry, school year |
config_security.py | Rate limiting, password rules, CSRF, upload checks, cookies |
config_integrations.py | Google Sheets, Drive storage, email, refresh intervals, renewals |
config_landing_page.py | Public page switches, hero text, feature list, logo, favicon |
Data is plain CSV in data/: instruments.csv, users.csv, checkout_history.csv (the audit trail, including where photos live) and signup_codes.csv. A school's inventory is hundreds of rows, not millions; files you can open in a spreadsheet beat a database server on school hardware.
Scheduled jobs (Sheets backup, request refresh, return reminders, the daily summary) start once per deployment behind a file lock. Turn them all off with BACKGROUND_JOBS_ENABLED = False.
Security
| Protection | Detail |
|---|---|
| Rate limiting | 5 sign-in attempts per 5 minutes per address |
| Passwords | bcrypt hashes; 8+ characters with upper, lower and a number |
| CSRF | Token required on every state-changing request |
| Injection | Templates escape input; CSV exports are neutralised against formula injection |
| Uploads | MIME sniffing and size limits; filenames are replaced |
| Sessions | HttpOnly, Secure and SameSite cookies |
| Audit | Sign-ins, edits and uploads are logged with who and when |
Each protection is a switch in config_security.py, which is useful while debugging and unwise in production. Never commit config/, google_ServiceAccount/, data/ or logs/; the repository ignores them already.
Questions
Do students need accounts?
No. Students browse the public catalog and request instruments through the Google Form. Only staff sign in. Public pages can be switched off entirely for an admin-only deployment.
How do other staff get accounts?
An admin generates an invite code with an expiry from the account menu. The new user redeems it on the sign-up page.
Do I need any of the Google integrations?
No. Sheets backup, Forms intake, Drive storage and email are separate switches. Inventory and manual checkouts work with nothing else configured.
Why does the wizard refuse my admin email?
It refuses admin@example.com because that is the published example. Shipping it would put a known login on your live system.
What happens when Drive fills up?
The storage manager reads real usage from the API, compresses older photos to local disk as it approaches the thresholds, warns on the dashboard, and past 90% can remove old cloud copies, never touching anything under six months old.
Can I move to a new server?
Copy the five folders. Everything is in them. Or restore the instruments from the backup sheet after a fresh install.
When something breaks
ModuleNotFoundError: No module named 'fcntl' on Windows
The background-jobs lock uses fcntl, which only exists on Unix. Set BACKGROUND_JOBS_ENABLED = False in config/config.py, or run the container or WSL. Linux deployments are unaffected.
"charmap codec can't encode" spam on Windows
Startup log lines contain characters cp1252 consoles cannot print. Harmless; silence it with set PYTHONIOENCODING=utf-8 (PowerShell: $env:PYTHONIOENCODING='utf-8').
Port already in use
lsof -ti:5000 | xargs kill -9. On Windows, Get-NetTCPConnection -LocalPort 5000 then Stop-Process -Id <pid>. The dev server uses port 5001; check the "Running on" line.
Cannot sign in as admin
After the wizard, the credentials are the ones from step 1, not the example ones. Check ADMIN_EMAIL in config/config.py. Five bad attempts lock you out for five minutes.
Google Sheets backup does nothing
Three checks: share the spreadsheet with the service-account email as Editor, make sure the spreadsheet ID and worksheet name match the settings, and confirm the credentials path exists. Details are in logs/app.log.
Emails are not sent
Use a Gmail app password (needs 2-Step Verification), never the account password. If the school account blocks SMTP, use a standalone Gmail. The SMTP conversation is in logs/email.log.
Form photos do not appear in requests
The form needs a file-upload question, the worksheet name must match GOOGLE_FORMS_CHECKOUT_WORKSHEET_NAME, and the response column must contain drive.google.com links. For permission errors, share the form's response folder with the service-account email as Viewer.
Storage dashboard shows 0% Drive usage
The service account needs the drive.readonly scope and the Google API client must be installed. Check the credentials path in config_integrations.py.
The full reference, including every configuration key, is in DOCUMENTATION.md in the repository.