Deployment Guide
SkoolyHub is a standard Next.js application: one Node process and one PostgreSQL database. Vercel, a container, or a plain VPS all work.
Before you deploy
-
DATABASE_URLandJWT_SECRETset in the host's environment -
NODE_ENV=production - The database reachable from the host
- Object storage connected (Settings → Channels → Storage)
- Admin password changed from anything auto-generated
-
npx next buildpasses locally
Environment
DATABASE_URL="postgresql://user:password@host/dbname?sslmode=require"
JWT_SECRET="a-long-random-string"
NODE_ENV="production"
JWT_SECRET must be stable and secret in production. It signs every session, and it derives the key that decrypts stored integration credentials. Rotating it signs everyone out and makes previously stored storage and payment credentials unreadable until they are re-entered.
The app fails fast at boot if it is unset in production rather than silently falling back to a default.
Database
Use the pooled connection string for the application — pooled connections suit serverless and short-lived processes.
Use the direct (unpooled) endpoint for pg_dump, pg_restore and any bulk load. The PgBouncer pooler stalls large single-transaction restores.
Storage is not optional
Uploads — student and staff photos, applicant photos, branding, gallery media, documents — go to object storage, never to local disk.
On serverless hosts the application filesystem is read-only at runtime, so there is no fallback: configure storage before going live or every upload fails.
Credentials live encrypted in the database, so they are configured through the admin once and then travel with the database — there is nothing storage-related to set in the host's environment.
Vercel
- Import the repository.
- Set
DATABASE_URL,JWT_SECRET,NODE_ENVunder Project → Settings → Environment Variables. - Deploy. Build command and output are detected.
- Visit
/installon the deployed URL if this is a fresh database.
The wizard cannot write .env on Vercel, so set the variables in the dashboard instead.
Container
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
Pass DATABASE_URL and JWT_SECRET at runtime rather than baking them into the image.
VPS
npm ci
npm run build
pm2 start npm --name skoolyhub -- start
Put nginx or Caddy in front for TLS and proxy to port 3000.
After deploying
- Sign in at
/adminand change the password. - Confirm Settings → Channels → Storage is connected on this deployment's database — a common mistake is configuring an add-on on staging and expecting production to see it.
- Connect a payment gateway if the school takes fees online.
- Check
/api/docs/v1renders, so the app team has the reference.
Multiple environments
Each environment is a separate database. Integration credentials live in the database, so:
- Connecting a gateway on staging does not configure production.
- Restoring a production dump into staging brings production's credentials with it — including live payment keys. Rotate them, or restore into a deployment with a different
JWT_SECRETso they simply do not decrypt.
Scaling
The app is stateless; sessions are JWTs. Run several instances behind a load balancer without sticky sessions.
The database is the limit. Every list endpoint is indexed on its filter and sort, and home screens are one round trip rather than a fan-out, so a school-sized workload is comfortable on a small instance.