# Deploy

This is the private MVP deployment runbook. Production smoke remains gated by `IGS-30`.

Architecture:

- Railway runs the Node/Fastify API from the repo `Dockerfile`.
- Cloudflare manages DNS, HTTPS proxying, WAF/bot protections, and the public hostname.
- Private MVP state uses a Railway persistent volume mounted at `/data` per decision `0011`.

## Required Checks

Run before deploy:

```bash
npm run check
```

The check runs lint, TypeScript build, and all tests.

## Required Environment

Core:

- `NODE_ENV=production`
- `PORT`
- `SERVICE_VERSION`
- `IGSKILL_STATE_FILE`
- `STALE_RESERVATION_MAX_AGE_MS`

Provider:

- `HIKERAPI_KEY`
- `APIFY_TOKEN`
- `BRIGHTDATA_API_KEY` if Bright Data fallback is enabled.
- `IGSKILL_PROVIDER_ORDER`, default `hikerapi,brightdata,apify`.
- `IGSKILL_PROVIDER_FAILOVER_ENABLED`, default `true`.

Auth and billing:

- `OWNER_ADMIN_KEY_SHA256`
- `IGSKILL_SIGNUP_INVITE_CODE`
- `STRIPE_SECRET_KEY`
- `STRIPE_WEBHOOK_SECRET`
- `STRIPE_TOPUP_PRICE_ID`
- `STRIPE_SUCCESS_URL`
- `STRIPE_CANCEL_URL`
- `PROVIDER_FLOAT_ALERT_MICRO_CREDITS`

Hosted AI:

- `ACHRONON_AI_ENDPOINT`
- `ACHRONON_AI_SERVICE_TOKEN`
- `ACHRONON_AI_TRANSCRIBE_PATH`, default `/claudex/transcribe`
- `ACHRONON_AI_TRANSCRIBE_MODEL`, default `auto`
- `ACHRONON_AI_TRANSCRIBE_MAX_DURATION_SEC`, default `180`

For the ClaudeVPS sandbox shape used by xskill, set `ACHRONON_AI_ENDPOINT` to the sandbox base, for example `https://api.claudevps.com/sandboxes/<sandbox-id>`. igskill appends `ACHRONON_AI_TRANSCRIBE_PATH` for transcription fallback.

## Startup

Build and start:

```bash
npm run build
npm run start
```

The app exposes:

- `GET /` public signup/top-up/docs surface
- `GET /docs`
- `GET /docs/:doc` for allowlisted markdown docs
- `GET /health`
- `POST /v1/signup`
- `GET /v1/post`
- `GET /v1/reel`
- `GET /v1/comments`
- `POST /v1/topups/checkout`
- `POST /v1/stripe/webhook`
- `GET /v1/ops/metrics`

## Railway

Deploy config lives in `railway.json`:

- Builder: `DOCKERFILE`.
- Runtime start command: `npm run start`.
- Health check: `GET /health`.
- Restart policy: retry on failure.

Railway setup:

1. Create a Railway service from this repo.
2. Attach a persistent volume mounted at `/data`.
3. Set `IGSKILL_STATE_FILE=/data/igskill-state.json`.
4. Set all required environment variables from `.env.example`.
5. Keep replicas at `1` while using JSON state.
6. Deploy and wait for the `/health` health check to pass.
7. Back up `/data/igskill-state.json` before replacing or migrating the service.

Do not scale horizontally until the state file is replaced by a database or another shared storage boundary.

## Cloudflare

Cloudflare setup:

1. Create a DNS record for the chosen hostname, for example `igskill.achronon.com`.
2. Point the record to the Railway public domain using a proxied CNAME when Railway provides one.
3. Enable HTTPS and keep the record proxied through Cloudflare.
4. Add WAF/bot controls before public signup is opened.
5. Keep `/v1/stripe/webhook` reachable by Stripe.
6. Avoid caching authenticated API responses at Cloudflare.
7. Optionally cache only static docs/landing responses after verifying no secrets are included.

Recommended Cloudflare cache posture:

- Bypass cache for `/v1/*`.
- Bypass cache for `/health`.
- Cache static `/docs/*.md` and `/` only if deployment headers are later added explicitly.

## State

The private MVP uses a single-replica JSON state file when `IGSKILL_STATE_FILE` is set. The file stores account metadata, key hashes, ledger entries, reservations, and cache-index placeholders. Treat it as sensitive operational state and back it up before replacing a deployment.

On startup, stale pending reservations older than `STALE_RESERVATION_MAX_AGE_MS` are refunded.

## Smoke

Without provider credentials, run:

```bash
npm run check
```

With `HIKERAPI_KEY`, run:

```bash
npm run smoke:hikerapi
```

With `APIFY_TOKEN`, run the specialized fallback smoke:

```bash
npm run smoke:apify
npm run smoke:apify:comments
```

Production smoke should cover health, signup, top-up checkout, post, reel, comments, parse, transcript, webhook credit grant, and ops metrics before public launch. Transcript smoke requires `transcript=true`, HikerAPI media URLs, and a working ClaudeVPS `claudex/transcribe` endpoint.

Run production smoke:

```bash
IGSKILL_SMOKE_BASE_URL=https://igskill.example.com \
IGSKILL_SMOKE_INVITE_CODE=... \
IGSKILL_SMOKE_OWNER_KEY=igsk_... \
IGSKILL_SMOKE_POST_URL=https://www.instagram.com/p/.../ \
IGSKILL_SMOKE_REEL_URL=https://www.instagram.com/reel/.../ \
IGSKILL_SMOKE_CHECKOUT=true \
IGSKILL_SMOKE_STRICT=true \
npm run smoke:production
```

Smoke behavior:

- `health` and disposable signup always run.
- Paid route checks use `IGSKILL_SMOKE_OWNER_KEY` so reads are unbilled.
- `raw post`, `comments`, and `parse` require `IGSKILL_SMOKE_POST_URL`.
- `raw reel` and transcript checks require `IGSKILL_SMOKE_REEL_URL`.
- Checkout runs only when `IGSKILL_SMOKE_CHECKOUT=true`.
- Strict mode fails when required production checks are skipped.
- Output redacts `igsk_` keys and bearer tokens.
