Administration and operations
Configuration
Environment variables, what happens when secrets change, and how to manage admin accounts.
On this page
Where settings live#
- All settings are environment variables in
.envin the app folder, for example/var/www/boolanga-lms/.env..env.examplelists them with comments. - After changing
.env, restart the app:sudo -iu lms pm2 restart boolanga-lms. No rebuild is needed. - The
npm runscripts (migrate, seed, reminders) read the same.envfrom the current folder, so run them from the app folder. - Keep
.envowned bylmsand readable only by it (chmod 600 .env), out of Git, and backed up in a password manager or secrets vault. It holds the secrets that protect rider PINs.
Generate a random secret with:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Environment variables#
| Variable | Needed | Default | What it’s for |
|---|---|---|---|
APP_URL | Yes | http://localhost:3000 | Public HTTPS address: https://academy.example.com, or https://<SERVER_IP> while there is no domain. Used to build one-tap links; with https:// sign-in cookies are marked Secure. Links already sent keep the old address, so keep the old one serving the site for 14 days after a change. |
SESSION_SECRET | Yes | - | Random string of at least 16 characters (use 64 hex characters). Signs admin and rider sessions and one-tap links. Also protects rider PINs unless PIN_SECRET is set. In production, sign-in and links fail without it. |
PIN_SECRET | Recommended | SESSION_SECRET | Separate secret (at least 16 characters) that protects rider PINs, so SESSION_SECRET can be changed without resetting PINs. Set it before riders get PINs and never change it. Read the warning below before adding it to a platform that already has riders. |
DATABASE_URL | On servers | empty | PostgreSQL connection string, for example postgres://lms:[email protected]:5432/boolanga_lms. Empty means the embedded preview database. |
DATABASE_POOL_SIZE | No | 10 | Maximum PostgreSQL connections the app opens. |
PGLITE_DIR | No | ./data/pglite | Folder of the embedded preview database. Not used when DATABASE_URL is set. |
ANTHROPIC_API_KEY | For AI | - | Claude API key. Needed for course generation, translation, reading PDFs for the assistant, and the assistant. Without it those features are switched off. |
INTEGRATION_API_KEY | For the API | - | Shared key your systems send in the x-api-key header. Without it, /api/v1 answers 503. |
ADMIN_EMAIL | For setup | [email protected] | Email of the first admin, created by npm run db:seed. |
ADMIN_PASSWORD | For setup | a known demo password | Password of the first admin. Always set a strong one: when empty, the seed uses a demo password written in the project’s README. Only read by the seed; changing it later doesn’t change the password. |
REMINDER_EVERY_DAYS | No | 3 | Days between reminders for unfinished training. A whole number, 1 or more. |
DEMO_LOGIN | No | off | on shows a Demo accounts box on both sign-in pages: one tap signs in as a demo rider (100100, 100200, 100300), the first admin (ADMIN_EMAIL) or the demo Qatar lead, without a PIN or password. Anyone who can open the site can use it, so only switch it on for demos, never with real riders or staff. Restart the app after changing it. |
WHATSAPP_TOKEN | No | - | WhatsApp Cloud API access token. With WHATSAPP_PHONE_NUMBER_ID, messages are sent automatically. |
WHATSAPP_PHONE_NUMBER_ID | No | - | ID of the sending phone number in Meta Business Manager. |
WHATSAPP_TEMPLATE_NAME | No | course_assigned | Approved template with two body variables: {{1}} first name, {{2}} link. |
WHATSAPP_TEMPLATE_LANGUAGE | No | en | Language code of that template. |
WHATSAPP_API_VERSION | No | v23.0 | Graph API version used for sending. |
NODE_ENV=production is set by the PM2 file ecosystem.config.cjs. Don’t put it in .env.
Never share real values
If SESSION_SECRET changes#
| What | Effect | What to do |
|---|---|---|
| Admins | Everyone is signed out. | Sign in again. Passwords still work. |
| Riders | Every rider is signed out. | Riders sign in again with PIN or a new link. |
| One-tap links already sent | Stop working. Riders see “That link has expired”. | Send new links (rider pages, flags, or the next reminders). |
| Rider PINs, without PIN_SECRET | Stop working. | Give riders new PINs: Reset PIN on each rider page, or import a CSV (or call POST /api/v1/riders) with a pin for each rider. |
| Rider PINs, with PIN_SECRET | Keep working. | Nothing. |
| Data, courses, records | Unchanged. | Nothing. |
Adding PIN_SECRET later also changes PINs
PINs set before PIN_SECRET existed are protected with SESSION_SECRET. If you add a new, different PIN_SECRET, those PINs stop working.
To separate them safely on a running platform: set PIN_SECRET to the current value of SESSION_SECRET, restart, and only then change SESSION_SECRET. PINs keep working.
Change the secrets if they may have leaked, or when someone with access to the server leaves.
Changing the other keys#
| Key | Effect of changing it |
|---|---|
INTEGRATION_API_KEY | Your systems must use the new key straight away. Coordinate the change. |
ANTHROPIC_API_KEY | None for users. The restart stops AI jobs that are running at that moment; they show as failed after about 15 minutes and can be retried. Restart at a quiet time. |
WHATSAPP_TOKEN | None for users, if the new token is valid. |
Database password | Change it in PostgreSQL and in DATABASE_URL at the same time, then restart. The backup job runs as postgres and needs no password. |
Admin accounts#
There is no screen for admin accounts yet. The first admin is created by npm run db:seed -- --admin-only on an empty database, from ADMIN_EMAIL and ADMIN_PASSWORD. To add accounts, change passwords, or change a market manager’s markets, use SQL on the server as below.
| Column | Values |
|---|---|
email | Lower case. Used to sign in. |
name | Shown in the admin menu. |
password_hash | bcrypt hash of the password (never the password itself). |
role | admin (every market) or market_manager. |
markets | For market managers: market codes, for example {QA} or {QA,BH}. Empty for admins. |
Add an account#
On the server, go to the app folder and create a password hash. The password isn’t shown or saved in the shell history.
bashcd /var/www/boolanga-lms read -rs -p "New password: " PW; echo HASH=$(PW="$PW" node -e "console.log(require('bcryptjs').hashSync(process.env.PW, 10))") unset PWInsert the account. For a market manager, list their market codes; for an admin use role 'admin' and array[]::text[].
bashsudo -u postgres psql -d boolanga_lms \ -v email="[email protected]" -v name="Kuwait training lead" -v hash="$HASH" <<'SQL' insert into admins (email, name, password_hash, role, markets) values (lower(:'email'), :'name', :'hash', 'market_manager', array['KW']); SQLShare the password with the person privately. They sign in at /admin/login.
Change a password, markets or remove an account#
Create a hash as in step 1 above when changing a password, then run the statement you need:
sudo -u postgres psql -d boolanga_lms -v email="[email protected]" -v hash="$HASH" <<'SQL'
-- new password
update admins set password_hash = :'hash' where email = lower(:'email');
-- different markets
-- update admins set markets = array['KW','BH'] where email = lower(:'email');
-- remove the account
-- delete from admins where email = lower(:'email');
SQL- A new password doesn’t end sessions that are already open; they expire within 12 hours.
- Removing an account signs that person out at their next page load. Courses they created stay.
Check what’s switched on#
| Feature | How to tell |
|---|---|
| AI | Courses > Create course shows “AI generation is off” when ANTHROPIC_API_KEY is missing. |
| Integration API | Any /api/v1 call answers 503 when INTEGRATION_API_KEY is missing, 401 with a wrong key. |
| WhatsApp Cloud API | Create a link for a test rider with your phone number: “Sent to the rider on WhatsApp.” means it’s on. |
| Reminders | Check the cron job and its log file (see Maintenance). |
| Demo sign-in | /login and /admin/login show a “Demo accounts” box while DEMO_LOGIN is on. |