# Staging Deployment Guide — AWS Lightsail

Step-by-step: create the server (Part A, you, ~15 min in a browser), deploy the
platform (Part B, runnable over SSH, ~1–2 hrs), load the data and verify
(Part C). Written so any engineer can execute it end to end.

Target: `https://staging.shodh.net` — the full stack on one 2 GB instance:
PostgreSQL 16, Spring Boot API, Next.js UI, Caddy for HTTPS.

## Why this shape — read before moving it anywhere else

One server with a real disk, not a managed platform. Three properties of the
application decide that, and each fails quietly rather than loudly if ignored:

1. **The filesystem must survive restarts.** Uploaded manuscripts are written to
   local disk (`LocalStorageServiceImpl`), not object storage. On a platform with
   an ephemeral filesystem — Vercel, Cloud Run, Heroku dynos, most "deploy from
   git" tiers — authors' submissions disappear on every redeploy, with no error.
2. **Exactly one instance.** The AI, password-reset and contact-form rate limits
   are per-JVM in-memory maps, so a second instance doubles every limit. (The
   two nightly cron jobs are idempotent prunes and Flyway serialises migrations
   behind its own lock, so neither is a problem; the rate limits alone settle it.)
3. **It must stay awake.** Mail, audit writes, request logging and the AI
   originality screen all run `@Async` *after* the response is sent, and the
   retention jobs fire at 03:15 and 03:25 IST. Scale-to-zero loses that work.

Also: the two image builds peak near 450 MB (API) and 700 MB (UI), measured, so
build them one at a time on a 2 GB box; uploads go to 30 MB so any proxy
body limit must exceed that, and Part C needs shell access (`psql`, `rsync`)
rather than only a git push.

Object storage would relax (1) — `STORAGE_TYPE=s3` is supported — but it is
untested in this deployment, so staging and production both use local disk.

---

## Part A — Create the server (browser, no command line)

### A1. AWS account & region
1. Sign in at https://lightsail.aws.amazon.com (create an AWS account if needed —
   card required, but this instance is $12/month and deletable anytime).
   Register the account with an **Indian address**: AWS then contracts through
   AWS India (AISPL) and bills in **INR with a GST invoice** rather than USD.
   Worth confirming on the first invoice.
2. Top-right region selector → **Mumbai (ap-south-1)**. Region is a latency
   decision — roughly 30 ms from India versus 250 ms from Europe.
3. Billing → **set a budget alert** (e.g. ₹1,500/month). Five minutes, and it
   turns a surprise bill into an email. The Lightsail plan is a flat monthly
   bundle including 1.5 TB of transfer in Mumbai, so overage is unlikely — but the
   alarm covers anything else that gets switched on later.

### A2. Create the instance
1. **Create instance** → Platform: *Linux/Unix* → Blueprint: **OS Only → Ubuntu 24.04 LTS**.
2. Instance plan: the **2 GB RAM / 2 vCPU / 60 GB SSD** plan.
3. Expand **+ Add launch script** and paste the block below. Lightsail runs it as
   root on first boot, which installs Docker before anyone logs in and makes
   step B1 unnecessary. Skipping this is not fatal — B1 does the same thing over
   SSH — but doing it here saves a reconnect, because the docker group only
   applies to a new login session.
   ```bash
   #!/bin/bash
   set -e
   # Docker Engine + the compose plugin, from Docker's own installer.
   curl -fsSL https://get.docker.com | sh
   # Let the default user run docker without sudo.
   usermod -aG docker ubuntu
   # Where Part B rsyncs the code and data.
   mkdir -p /home/ubuntu/shodh/uploads /home/ubuntu/shodh/logs
   chown -R ubuntu:ubuntu /home/ubuntu/shodh
   # A cushion for rebuilds: both image builds at once, beside the running
   # site, can pass 2 GB. B5 builds them one at a time; swap covers the rest.
   fallocate -l 2G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
   echo '/swapfile none swap sw 0 0' >> /etc/fstab
   ```
4. Name it `shodh-staging` → **Create instance**. Wait until it shows *Running*.
   The launch script keeps running for a minute or two after that.

### A3. Static IP (so the address never changes)
1. Networking tab (left menu) → **Create static IP** → attach it to `shodh-staging`.
2. Note the IP — call it `STATIC_IP` below.

### A3b. Turn on automatic snapshots
Instance → **Snapshots** tab → enable automatic snapshots.

The archive is the irreplaceable part of this system — 479 articles reconstructed
from a legacy site that is being retired. Snapshots cost a few rupees a month and
are the difference between an afternoon's restore and starting again.

### A4. Open the web ports
1. Open the instance → its **Networking** tab → IPv4 Firewall.
2. Add rule: **HTTP (TCP 80)**. Add rule: **HTTPS (TCP 443)**. (SSH 22 is already there.)

### A5. Download the SSH key
1. Account menu (top right) → **Account** → *SSH keys* tab → download the
   **default key for ap-south-1** (a `.pem` file).
2. On the Mac: move it somewhere stable and lock its permissions:
   ```bash
   mkdir -p ~/.ssh && mv ~/Downloads/LightsailDefaultKey-ap-south-1.pem ~/.ssh/ && chmod 600 ~/.ssh/LightsailDefaultKey-ap-south-1.pem
   ```

### A6. DNS record (shodh.net cPanel — DNS only, nothing else)
1. cPanel → **Zone Editor** → shodh.net → **+ A Record**:
   - Name: `staging` · TTL: `300` · Address: `STATIC_IP`
2. Done when `ping staging.shodh.net` answers with the static IP (can take a few minutes).

**Hand-off point:** share `STATIC_IP` and the key file path — Part B onward runs
from the development machine over SSH.

---

## Part B — Deploy the stack (over SSH)

Shorthand used below:
```bash
SSH="ssh -i ~/.ssh/LightsailDefaultKey-ap-south-1.pem ubuntu@STATIC_IP"
```

### B1. Confirm Docker is there
The launch script in A2 installed it. Check, and only install if that was skipped:
```bash
$SSH 'docker --version && docker compose version'
# If either is missing:
$SSH 'curl -fsSL https://get.docker.com | sudo sh && sudo usermod -aG docker ubuntu'
# (then reconnect once, so the docker group applies to the session)
```

### B2. Get the code onto the server
The repos are private, so push-from-dev is simplest (no GitHub keys on the server):
```bash
cd ~/Documents/Shodh-Sanchayan/ShodhShanchayanCodeBase
rsync -az -e "ssh -i ~/.ssh/LightsailDefaultKey-ap-south-1.pem" \
  --exclude node_modules --exclude .next --exclude target --exclude uploads \
  --exclude logs --exclude .git --exclude legacy-import --exclude '.env' --exclude '.env.*' \
  shodh-sanchayan-api shodh-sanchayan-ui ubuntu@STATIC_IP:/home/ubuntu/shodh/
```
The local `.env` holds live AI, mail and payment keys, and `legacy-import/`
holds registrants' contact details. Neither is needed on the server: Part C
sends the import files from this machine.

### B3. Copy the archive PDFs (168 MB, one-time)
```bash
rsync -az -e "ssh -i ~/.ssh/LightsailDefaultKey-ap-south-1.pem" \
  shodh-sanchayan-api/uploads/archive ubuntu@STATIC_IP:/home/ubuntu/shodh/uploads/
```

### B4. Server-side configuration
Create `/home/ubuntu/shodh/.env` on the server (values below are staging-only;
generate a fresh `JWT_SECRET` with `openssl rand -hex 32`):
```env
SITE_ADDRESS=staging.shodh.net
PUBLIC_URL=https://staging.shodh.net
DB_PASS=<generate-one>
JWT_SECRET=<openssl rand -hex 32>
# Optional. Leave AI_API_KEY blank to run without AI: the endpoints return 503
# and the submit form hides its AI controls. It never falls back to fake text.
AI_PROVIDER=openai
AI_API_KEY=
# Outgoing mail. Setting MAIL_USER is the single switch that turns mail on —
# leave it blank and mail is a logged no-op, which also leaves password reset
# unavailable and its link hidden in the UI (a safe default, not a broken one).
# Gmail requires an App Password here, not the account password.
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USER=
MAIL_PASS=
MAIL_FROM_NAME=शोध संचयन / Shodh Sanchayan
```

Copy the committed compose file and Caddyfile next to the two repositories:
```bash
scp -i ~/.ssh/LightsailDefaultKey-ap-south-1.pem \
  shodh-sanchayan-api/deploy/docker-compose.yml shodh-sanchayan-api/deploy/Caddyfile \
  ubuntu@STATIC_IP:/home/ubuntu/shodh/
```
They are committed rather than pasted into this guide, so the file rehearsed in
B4b is the file that runs on the server. It caps container logs, serves `/api`
and the UI on one origin (no CORS to get wrong in the browser), lets uploads
reach 30 MB, and restarts every service by itself after a reboot.

### B4b. Rehearse it on a laptop first (optional, recommended)

The same compose file runs locally with no domain and no TLS, which is how you
find a broken Dockerfile or a wrong build argument before doing it over SSH:

```bash
cp shodh-sanchayan-api/deploy/{docker-compose.yml,Caddyfile} .
DB_PASS=local JWT_SECRET=$(openssl rand -hex 32) \
SITE_ADDRESS=:80 PUBLIC_URL=http://localhost:8080 CADDY_HTTP_PORT=8080 CADDY_HTTPS_PORT=8443 \
  docker compose -p shodh-local up -d --build
curl -s http://localhost:8080/api/actuator/health   # → {"status":"UP"}
docker compose -p shodh-local down -v               # removes the throwaway data
```

`SITE_ADDRESS=:80` makes Caddy serve plain HTTP and skip certificates entirely —
Let's Encrypt cannot issue for localhost. TLS is the one part that can only be
tested against a real domain.

### B5. Build and start
```bash
$SSH 'cd shodh && docker compose build api && docker compose build ui && docker compose up -d'
```
Building one image at a time keeps a 2 GB box inside its memory. The first
build is slow on a small instance. Confirm:
```bash
$SSH 'cd shodh && docker compose ps'
curl -s https://staging.shodh.net/api/actuator/health   # → {"status":"UP"}
```
Flyway applies V1–V36 automatically on the API's first start.

---

## Part C — Load data & verify

### C1. UAT users (two per role — credentials in `docs/testing-guide.md`)
```bash
$SSH 'cd shodh && docker compose exec -T postgres psql -U shodh -d shodh_sanchayan' \
  < shodh-sanchayan-api/scripts/seed-uat-users.sql
```

### C2. Archive + registries (idempotent imports via the admin API)
```bash
B=https://staging.shodh.net/api
TOKEN=$(curl -s -X POST $B/auth/login -H 'Content-Type: application/json' \
  -d '{"email":"sampadak1@test.shodh.net","password":"Sampadak@2026"}' | python3 -c 'import json,sys;print(json.load(sys.stdin)["token"])')
curl -s -X POST $B/admin/archive/import  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' --data-binary @shodh-sanchayan-api/legacy-import/manifest-enriched.json
curl -s -X POST $B/admin/archive/import  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' --data-binary @shodh-sanchayan-api/legacy-import/manifest-recovered.json
curl -s -X POST $B/admin/registry/import -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' --data-binary @shodh-sanchayan-api/legacy-import/backup/registry-import.json
```
Expected, in order: 25 issues + 477 articles created; 2 issues updated + 2
articles created (recovered after the first crawl); and
`{"created":7575,"published":451,"repaired":22,"updated":0}`.

The registry import repairs the text the legacy export double-encoded and
publishes the supervisors that pass the checks in migration V35, so nothing
needs running after it. Both migrations only reach rows that already exist,
which on a fresh server is none. Afterwards the archive holds 479 articles and
314 supervisors are hidden.

### C3. Dr. Singh's real admin account
Create with a one-time password and forced change on first login:
```sql
-- via docker compose exec postgres psql …
INSERT INTO users (id, email, password_hash, name_hi, name_en, role, status,
                   email_verified, must_change_password)
VALUES (uuid_generate_v4(), '<his-email>', '<bcrypt-of-one-time-password>',
        'डॉ. वाई. पी. सिंह', 'Dr. Y. P. Singh', 'ADMIN', 'ACTIVE', TRUE, TRUE);
```
(Generate the hash on the Mac: `htpasswd -bnBC 10 "" 'OneTimePw' | tr -d ':\n' | sed 's/\$2y/\$2a/'`.)

### C4. Verification gate (all must pass before sharing the URL)
```bash
# every legacy URL 301s correctly against the real domain
cd shodh-sanchayan-ui && node scripts/verify-redirects.mjs https://staging.shodh.net
```
- [ ] `https://staging.shodh.net` loads; language toggle flips the whole page
- [ ] Archive: open Vol.9/1 → download a PDF → counter increments
- [ ] Search "गाँधी" and "Telemedicine" both hit
- [ ] All six UAT logins land in the correct role
- [ ] Registry public pages show NO email/phone/address; admin view shows them
- [ ] Contact form submission appears in the admin inquiries inbox
- [ ] `sitemap.xml` and `robots.txt` serve
- [ ] Old-URL smoke test in a browser: `/index.php?option=com_content&view=article&id=21` → About page

### C5. Share with the customer
Update the walkthrough page's URL slot to `https://staging.shodh.net`, then send
Dr. Singh the walkthrough + the URL.

---

## Costs & housekeeping

- US $12/month plus about $1 of snapshots while the instance exists (Lightsail bills stopped instances
  too — **delete** it after UAT to stop billing; take a snapshot first if wanted).
- Payments run in test mode on staging (no live Razorpay keys configured).
- This guide deploys **staging**; production adds: live Razorpay + SMTP keys,
  a backup strategy for Postgres, and the DNS cutover steps in
  `docs/legacy-site/cutover-checklist.md`.
