# Deploying i-Learned to Railway

A step-by-step for putting the system online at your own subdomain, for a client
demonstration.

Railway because it runs Node and PostgreSQL in one project, and does not sleep
an idle service the way some free tiers do — a demo that takes thirty seconds to
wake is a demo that has already gone wrong. Any host with Node 20+ and
PostgreSQL works the same way; only the dashboard differs.

Every command below was run against this codebase before it was written. Where
something is a known trap, it says so.

---

## Before you start

You need:

- A **GitHub account** — Railway deploys from a repository.
- A **Railway account** (railway.app), signed in with GitHub.
- Access to **DNS for ichthuslance.com**, to point the subdomain.

Budget roughly 30 minutes for the first deploy.

> **A note on cost.** Railway's trial credit covers a demonstration comfortably.
> The app and the database are billed separately by usage; a demo instance is
> cents per day, not dollars.

---

## 1 — Put the code in a repository

The project is not yet a Git repository, and no host can deploy from a folder.

```bash
cd D:\SchoolManagementSystem
git init
git add -A
git commit -m "i-Learned: initial commit"
```

Then create an **empty private repository** on GitHub — no README, no
`.gitignore`, it must be empty — and push:

```bash
git remote add origin https://github.com/YOUR-USERNAME/i-learned.git
git branch -M main
git push -u origin main
```

**Keep it private.** The repository contains no secrets — `.env` and the local
database directory are both ignored, which was checked — but the codebase is
your work and there is no reason to publish it.

> Verify before pushing: `git status --short | findstr /I ".env pgdata node_modules"`
> should print nothing.

---

## 2 — Create the project and the database

1. Railway → **New Project** → **Deploy from GitHub repo** → choose your
   repository.
2. Once it appears, click **+ New** → **Database** → **Add PostgreSQL**.

You now have two services side by side: your app, and a Postgres instance.
Railway sets `DATABASE_URL` on the database service; you will reference it from
the app in the next step.

The first build will fail or produce a broken app until step 3 is done. That is
expected — it has no configuration yet.

---

## 3 — Configure the app service

Open the app service → **Settings**.

**Build command**

```
pnpm install --frozen-lockfile --prod=false && pnpm build
```

`--prod=false` matters: `NODE_ENV` is `production` on the service, and pnpm
would otherwise skip `devDependencies` — where `prisma`, `typescript` and
`dotenv-cli` live. The install succeeds and the build then fails on a missing
command.

**Start command**

```
pnpm --filter @i-learned/web start
```

**Watch paths** — leave blank so any change redeploys.

Node version is already pinned by `package.json` (`>=20.11.0`) and the package
manager by `packageManager: pnpm@9.15.4`, so Railway picks both up without help.

---

## 4 — Set the environment

App service → **Variables**. Add these.

| Variable | Value | Why |
| --- | --- | --- |
| `DATABASE_URL` | `${{Postgres.DATABASE_URL}}` | Railway substitutes the database's own URL. Type it exactly, including the braces. |
| `SESSION_SECRET` | *(generate — see below)* | Signs session tokens. Must be at least 32 characters or the app refuses to start. |
| `APP_URL` | `https://ilearned.ichthuslance.com` | **Set this before the first deploy.** Password-reset and account-setup links are built from it; a wrong value produces links that go nowhere. |
| `SESSION_COOKIE_SECURE` | `true` | Without it, session cookies travel unencrypted. |
| `NODE_ENV` | `production` | |
| `TZ` | `Asia/Manila` | Timestamps on documents and audit entries. |

Generate the secret locally and paste the output:

```bash
node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))"
```

**Do not put this secret anywhere but the Variables panel.** Not in the
repository, not in a chat message, not in the proposal.

### Optional — real email

Without these, the app says plainly that nothing was emailed and shows the setup
link on screen instead, which is fine for a demo. To send real mail:

| Variable | Value |
| --- | --- |
| `MAIL_DRIVER` | `smtp` |
| `SMTP_HOST` | `smtp.gmail.com` |
| `SMTP_PORT` | `587` |
| `SMTP_USER` | your address |
| `SMTP_PASSWORD` | a Google **app password**, not your account password |
| `MAIL_FROM` | `i-Learned <you@example.com>` |

---

## 5 — Create the schema

Railway → app service → the **⋮** menu → **Shell** (or `railway run` from the
CLI). Then:

```bash
pnpm db:migrate:deploy
```

This creates all 107 tables, the constraints, and the append-only audit triggers.

Then choose **one** of the following.

**For a demonstration with realistic data** — 80 students, applications,
payments and grades already in place:

```bash
pnpm db:seed
```

**For an empty system you fill in yourself** — keeps roles, permissions and
settings, creates one superadmin:

```bash
pnpm db:seed && pnpm db:clear-demo --apply
```

Seed first either way: `db:clear-demo` keeps the configuration the seed creates
(permissions, roles, admission tracks, grading scales) and clears only the
demonstration content.

> The seed refuses to run when `NODE_ENV=production`. For a demo instance, set
> `NODE_ENV=development` temporarily, seed, then set it back and redeploy. This
> guard exists so nobody ever seeds fictional students over a real institution's
> database — do not remove it.

---

## 6 — Point your subdomain at it

**In Railway:** app service → **Settings** → **Networking** → **Custom Domain** →
enter `ilearned.ichthuslance.com`. Railway shows a target hostname such as
`something.up.railway.app`.

**In your DNS** (cPanel → Zone Editor, since the domain lives there):

| Type | Name | Value |
| --- | --- | --- |
| CNAME | `ilearned` | the hostname Railway gave you |

Save, then wait. Propagation is usually minutes but can take up to an hour.
Railway issues the TLS certificate automatically once it sees the record.

Check it has taken effect:

```bash
nslookup ilearned.ichthuslance.com
```

---

## 7 — Verify before showing anyone

Work through these in order. Each one has failed at least once during
development, which is why they are listed.

1. `https://ilearned.ichthuslance.com/login` loads over **https**, with a padlock.
2. Sign in as the superadmin. If you seeded, that is `superadmin@example.local`
   with the demo password; if you cleared, the same account was recreated.
3. `/privacy` loads **signed out** — open it in a private window.
4. **Administration → Users → Add user.** Create an account and check the setup
   link points at `ilearned.ichthuslance.com`, not `localhost`. If it says
   localhost, `APP_URL` is wrong; fix it and redeploy.
5. Open that setup link and set a password. This exercises the whole
   reset flow end to end.
6. **Registrar → Programs → Add programme.** Confirms the database is writable.
7. **Account → Your data → Download my data.** Confirms file responses work.

---

## Changing the demo password

The seeded accounts all use the same well-known password, which is fine on your
laptop and not fine on a public URL. Before sending the link to a client:

```bash
# In the Railway shell
node -e "console.log(require('crypto').randomBytes(9).toString('base64url'))"
```

Then sign in as superadmin and use **Users → Change password** on each account
you intend to demonstrate, or delete the ones you will not use.

Anyone who finds the URL can otherwise sign in as a student — and while the data
is fictional, a client discovering that during your demonstration is not the
impression you want.

---

## When something goes wrong

**Build fails on `pnpm install`.** Check the lockfile was committed —
`pnpm-lock.yaml` must be in the repository.

**App starts then crashes.** Read the deploy logs. A missing or short
`SESSION_SECRET` fails at startup by design, with a message saying so.

**Pages load but every action errors.** `DATABASE_URL` is wrong, or the
migration has not run. Confirm the variable reads exactly
`${{Postgres.DATABASE_URL}}`.

**Reset links point at localhost.** `APP_URL` was not set before the deploy.
Set it and redeploy — existing links stay broken, so issue new ones.

**The custom domain shows a certificate warning.** DNS has not propagated yet,
or the CNAME points at the wrong hostname. Re-check with `nslookup`.

---

## What this deployment is not

It is a **demonstration**, not a production system. Before it may hold a real
student's record, the conditions in `security-checklist.md` apply — multi-factor
authentication for privileged roles, encryption at rest, shared rate limiting,
malware scanning, off-host audit shipping, and the institutional approvals
including Data Protection Officer sign-off.

The standing banner and the watermark on generated documents say the data is
fictional. Leave them on.
