Administration and operations
Security notes
How sign-in, sessions and keys are protected, where personal data goes, known gaps, and a hardening checklist.
On this page
Sign-in and sessions#
| Who | Signs in with | Stored as | Session |
|---|---|---|---|
| Admins | Email and password | bcrypt hash | 12 hours |
| Riders | Rider ID and PIN (4 to 8 digits) | HMAC-SHA256 with PIN_SECRET, or SESSION_SECRET if not set | 30 days |
| Riders | One-tap link | Signed token (HS256, SESSION_SECRET), valid 14 days | 30 days |
| Operator systems | INTEGRATION_API_KEY header | Compared in constant time | Each request |
- Session cookies are
HttpOnlyandSameSite=Lax, andSecurewhenAPP_URLstarts withhttps://. - Sign-in is limited to 5 attempts per admin email or rider ID per 15 minutes. The counter is kept in memory: it resets when the app restarts and only works with a single app process.
- The integration API has no rate limit.
- Sessions can’t be ended one by one. A new admin password doesn’t end open sessions (they expire within 12 hours). Removing an admin account, or setting a rider to inactive, ends access at the next request. Changing
SESSION_SECRETsigns everyone out (effects).
PINs are short
A 4-digit PIN has only 10,000 combinations. PINs rely on the secret and the attempt limit. Prefer 6-digit PINs (the generated ones are 6 digits) and one-tap links.
One-tap links#
- Anyone who has a rider’s link can open that rider’s training for 14 days. Links aren’t single-use.
- A single link can’t be cancelled. To cut access, set the rider to inactive, or change SESSION_SECRET (all links).
- Links carry only a signed rider reference, not personal data.
- Link previews don’t sign anyone in. Known preview crawlers (WhatsApp, Slack, Discord, Twitter/X, Skype, Google and similar) get a page with only the app’s name, description and a sign-in link, with no session and no database access. In-app browsers (Telegram, LinkedIn, Facebook) are treated as the rider and sign in normally.
- Signed-out visitors to the rider app or the admin panel are redirected to the sign-in page before any page loads (
src/proxy.ts); each page still checks the session itself. - Send links only to the rider’s own phone number.
Roles and data access#
- Admins see and change everything. Market managers manage riders in their own markets and see figures, reports and exports for those markets only. Courses, learning paths and assistant knowledge are read-only for them. See what market managers see.
- Imports and new riders can’t change or move riders in markets outside the admin’s own markets.
- A proof of completion page opens for the rider it belongs to and for admins who can see that rider’s market.
- There is no audit log of admin actions.
- CSV exports contain names and phone numbers. Store downloaded files as personal data.
- The API key has full access to every market.
Personal data and where it goes#
| Data | Stored in | Sent to |
|---|---|---|
| Rider records: rider ID, name, phone, market, city, vehicle, nationality, language, status | PostgreSQL on your server | - |
| Training records, flags, activity log | PostgreSQL | - |
| Questions riders ask the assistant | PostgreSQL (activity log) | Claude API (Anthropic), to produce the answer |
| Course material, uploaded PDFs and SCORM text, knowledge documents | Course and document text in PostgreSQL. Uploaded files aren’t kept. | Claude API, for generation, translation, transcription and answers |
| Rider’s first name, phone number and link | Message log in PostgreSQL | WhatsApp Cloud API (Meta), only when connected |
Data residency depends on where the server runs. The Claude API and WhatsApp are external services.
Not built yet
There are no tools to export or delete one rider’s data for data protection requests, and no automatic data retention. Until then, IT can delete a rider and everything linked to them (flags, assignments, records, activity, messages) with SQL:
bash
sudo -u postgres psql -d boolanga_lms -c "delete from riders where rider_code = '300001';"Hardening checklist#
- HTTPS with a valid Let’s Encrypt certificate that renews automatically (
sudo certbot renew --dry-runpasses), port 80 only redirecting to HTTPS, andAPP_URLstarting withhttps://. Prefer a domain over the server’s IP address. See HTTPS. - Long random values for
SESSION_SECRET,PIN_SECRETandINTEGRATION_API_KEY, different from each other and from any demo server. ADMIN_PASSWORDset to a strong password before running the seed. No demo data on a live platform (demo riders use PIN 1234, and the demo market manager account shares the admin password).DEMO_LOGINoff (or not set). While it’s on, anyone who can open the site signs in as the first admin with one tap.- The app runs as the unprivileged system user
lms, never as root..envis owned bylmswith mode 600, never in Git. - nginx security headers from
deploy/nginx.confin place, and, once the site runs on a domain,Strict-Transport-Securityuncommented (browsers ignore it for IP addresses). - Firewall (
ufw) allowing only OpenSSH and Nginx Full. PostgreSQL and the app listen on 127.0.0.1 only. fail2banrunning with the sshd jail. SSH with keys only (password login disabled), and a short list of people with server access. See SSH access.- Automatic security updates for Ubuntu (
unattended-upgrades) and regular updates of Node.js and the app’s packages. - Backups run as
postgresinto a folder only it can read (mode 700), so no database password is written in a crontab. Copies are encrypted and stored off the server, and restores are tested. - Rotate secrets when people with access leave. See If SESSION_SECRET changes.