Troubleshooting

Setup

DATABASE_URL is not set No .env, or the process started before it existed. Restart after creating it.

Connection refused, or SSL required A managed database usually needs ?sslmode=require on the connection string.

The install wizard cannot write .env The filesystem is read-only — normal on Vercel. Set the variables in the host's environment instead.

A screen is empty and the console shows a 500 about a missing table That module's ensure*Schema() has not run. Visit any screen in the module, or run npm run db:init.

Uploads

"Storage is not configured" No active Storage connection. Admin → Settings → Channels → Storage.

It still says that after connecting You almost certainly have more than one database, and configured the add-on against a different one than this deployment reads. Check the DATABASE_URL the running app is using. This is the single most common support question.

502 SignatureDoesNotMatch The stored storage secret is wrong or the token was rotated. Re-enter the credentials — it is configuration, not code.

Uploads work locally, fail in production The production deployment's database has no storage connection of its own. They are stored per database, not per codebase.

Payments

No online option appears in the parent app No active Payments connection, or the connected provider has no registered driver. A gateway needs both.

A parent paid but the invoice is still unpaid Check the payment attempt. If it is pending, the gateway never confirmed — usually the parent closed the page before finishing, in which case nothing was charged and the invoice is correctly untouched. If it is failed, the provider declined it. The attempt's gateway_ref is what to give your provider's support.

Can a parent pay the wrong amount? They can pay less. They cannot pay more — the charge is computed server-side from the invoice and the request cannot raise it.

Authentication

A parent never receives their code Confirm the email or phone they are entering matches what the school holds. Codes last 10 minutes and work once. In development with no mail channel configured, the code is printed to the server log.

A teacher is locked out They reset it themselves at the teacher login — a code goes to their school email. No administrator needs to be involved, and none can see the password.

"unauthorized" on every app call after login The token is not being sent. Mobile clients need Authorization: Bearer <jwt>; the cookie is only set for the web portals.

A 401 on an endpoint that should work The token's role does not match the endpoint. A parent token on a teacher route is rejected by design.

Results and attendance

Published results are not visible to parents Both flags are required: the report card published and its exam at published status.

The attendance percentage looks too high It divides by days a register was actually recorded, not calendar days. A day nobody took the register counts against nobody.

Taking the register again created duplicates It should not — it overwrites on (student, date, period). If you genuinely see duplicates, the unique index is missing; re-run the schema provisioning.

Development

A route 404s although the file exists and typechecks Turbopack de-registered neighbouring routes after a folder was added. Touch the page.tsx or route.ts, or restart npm run dev. Almost never a code bug.

The first request to a new route takes several seconds Turbopack compiles on demand. Not a hang.

server-only throws in a script The script imported something that only resolves inside Next. Import the database client dynamically after loading env.

A sql template breaks with a syntax error at the literal A backtick inside a SQL comment terminated the template.

A threshold silently became 0 Number(null) is 0, not NaN, so Number(x) || fallback does not do what it looks like. Check presence explicitly.

Data

A student cannot be deleted They have fee history. Receipts already issued cannot be un-issued — set the status to transferred or withdrawn instead.

Two clerks admitted students and got the same number They should not. Leaving the admission number blank allocates it server-side; typing one manually is where collisions come from.

Last year's data looks wrong after promotion Check student_enrollments — the per-year record is what history resolves against, and it is written at promotion.