Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,61 @@ AZURE_OPENAI_API_VERSION=
# Legacy OpenAI key is unused for Stackfast's Azure-only setup but
# is kept for operators who swap in the standard OpenAI endpoint.
OPENAI_API_KEY=

# ── Rate limit backend (Phase 8) ──────────────────────────────────
# See docs/decisions/003-deployment-architecture.md § 3 "Rate-limit backend"
# and .kiro/specs/phase-8-deployment/design.md § "Configuration surface".
#
# memory — in-process, single instance only (dev default, also used by tests).
# upstash — distributed, survives restarts. Requires the two Upstash vars below.
RATE_LIMIT_BACKEND=memory

# Upstash Redis credentials (only read when RATE_LIMIT_BACKEND=upstash).
# Create a database at https://console.upstash.com; copy the REST URL + token.
# Leaving these unset while RATE_LIMIT_BACKEND=upstash triggers a one-time
# `[rate-limit] upstash env missing, falling back to memory` warn on boot
# (design § 9 step 2) so the API never fails to start.
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=

# ── Error tracking — Sentry (Phase 8, optional) ───────────────────
# See docs/decisions/003-deployment-architecture.md § 5 "Error tracking".
# Every Sentry variable below is optional. When SENTRY_DSN is empty the
# `initSentry()` calls on the API and web become silent no-ops.

# API Sentry project DSN (server-side @sentry/node).
SENTRY_DSN=

# Web Sentry project DSN (browser @sentry/react). This is exposed in
# the bundled JS, so only use the DSN, never any secret tokens.
VITE_SENTRY_DSN=

# Sentry Release tag. In production, Railway injects RAILWAY_GIT_COMMIT_SHA
# automatically; VITE_APP_RELEASE is read at Vite build time.
RAILWAY_GIT_COMMIT_SHA=
VITE_APP_RELEASE=

# Source-map upload credentials (only needed at build time, never at
# runtime). If any of these are missing, the Sentry Vite plugin is
# skipped and source maps stay local.
SENTRY_AUTH_TOKEN=
SENTRY_ORG=
SENTRY_PROJECT_API=
SENTRY_PROJECT_WEB=

# ── Web build-time API URLs (Phase 8, VITE_*) ─────────────────────
# See docs/decisions/003-deployment-architecture.md § 4 "Cookie and CORS
# strategy" and .kiro/specs/phase-8-deployment/design.md § "Configuration
# surface". These are read by Vite at BUILD time and baked into the bundle;
# they are never read by the API at runtime.
#
# Local dev does NOT use these root values — apps/web/.env.development sets
# them to the same-origin proxy paths (VITE_API_URL=/api/v1, VITE_AUTH_URL=/)
# so Better Auth cookies flow through Vite's proxy without CORS preflight.
#
# In PRODUCTION the web service does NOT proxy: it calls the API origin
# directly (R3.7), so these must be the absolute api.stackfast.app URLs:
# VITE_API_URL=https://api.stackfast.app/api/v1
# VITE_AUTH_URL=https://api.stackfast.app
VITE_API_URL=/api/v1
VITE_AUTH_URL=/
203 changes: 124 additions & 79 deletions .kiro/specs/phase-8-deployment/tasks.md

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions apps/api/drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { defineConfig } from "drizzle-kit";

/**
* Drizzle Kit config for the Stackfast API service.
*
* This config is consumed by `drizzle-kit push`, which is wrapped by the
* one-shot migration runner at `scripts/deploy/migrate.ts` (Phase 8, R2.4).
*
* Decision (design.md § "Open questions" item 2): Phase 8 uses `drizzle-kit
* push` rather than `drizzle-kit migrate`. The repo has no `drizzle/`
* migration history yet, and inventing one just to run `migrate` adds process
* weight without value. Promote to `migrate` (and add an `out` migrations
* folder) once the first real migration history appears post-MVP.
*
* The schema lives in the shared `@stackfast/schemas` package. `drizzle-kit`
* loads the schema by file path (not module specifier), so this points at the
* source file relative to this config's directory (apps/api).
*/
export default defineConfig({
dialect: "postgresql",
// Relative to apps/api — resolves to packages/schemas/src/db.ts.
schema: "../../packages/schemas/src/db.ts",
// `out` is only used by `drizzle-kit generate`/`migrate`. Declared here so a
// future switch to migration files has a home; `push` ignores it.
out: "./drizzle",
dbCredentials: {
// The migration runner validates this is set before invoking drizzle-kit,
// and waits for the connection to come up (R2.3) before pushing.
url: process.env.DATABASE_URL ?? "",
},
// Surface the SQL drizzle-kit intends to run. The runner adds `--strict` in
// --dry-run mode so nothing is applied without explicit approval.
verbose: true,
});
2 changes: 2 additions & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"dependencies": {
"@hono/node-server": "^1.13.7",
"@neondatabase/serverless": "^0.10.4",
"@sentry/node": "^10.53.1",
"@stackfast/ai": "workspace:*",
"@stackfast/exporter": "workspace:*",
"@stackfast/registry": "workspace:*",
Expand All @@ -30,6 +31,7 @@
},
"devDependencies": {
"@types/node": "^22.10.2",
"drizzle-kit": "^0.31.4",
"fast-check": "^4.8.0",
"tsx": "^4.19.2",
"typescript": "^5.6.3"
Expand Down
67 changes: 67 additions & 0 deletions apps/api/railway.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Railway service manifest for `stackfast-api`.
#
# Phase 8 deployment — ties back to:
# - ADR 003 § 1 "Hosting: split web + API, both on Railway"
# (docs/decisions/003-deployment-architecture.md)
# - design.md § "Railway service topology — API Service"
# (.kiro/specs/phase-8-deployment/design.md)
#
# Schema reference: https://railway.com/railway.schema.json
# Config-as-code docs: https://docs.railway.com/config-as-code/reference
#
# Node 20 runtime: Nixpacks auto-detects the Node major version from the
# root `package.json` "engines" field (">=20.0.0"). If the Nixpacks
# autodetection ever regresses, the operator can pin it explicitly via a
# Railway env var: `NIXPACKS_NODE_VERSION=20`. Pinning in the repo is
# intentionally deferred to a follow-up task so this manifest stays
# focused on build/run/healthcheck.
#
# Environment variables are NOT declared here — they are set per
# environment via the Railway CLI/dashboard, per ADR 003 § 6 and
# design § "Configuration surface". Example:
# railway variables \
# --set NODE_ENV=production \
# --set DATABASE_URL=... \
# --service stackfast-api \
# --environment production
# See Batch G (staging) and Batch H (production) in
# .kiro/specs/phase-8-deployment/tasks.md for the cutover runbook.

"$schema" = "https://railway.com/railway.schema.json"

[build]
# Nixpacks is the deterministic path: it reads the repo-root
# package.json "engines" to select Node 20 and runs pnpm from the repo
# root so workspace filters (`--filter @stackfast/api...`) resolve.
builder = "NIXPACKS"

# Explicit install+build pipeline from design § "API Service". The `...`
# suffix on the filter includes @stackfast/api's workspace dependencies
# (schemas, registry, rules-engine, exporter, ai, shared), so tsc's
# project references compile against the built packages.
buildCommand = "pnpm install --frozen-lockfile --filter @stackfast/api... && pnpm --filter @stackfast/api build"

[deploy]
# Hono server entrypoint (`node dist/index.js` via the package's `start`
# script, wrapped in `dotenv -e ../../.env --` for local parity; on
# Railway the dotenv step is a no-op because `.env` is not shipped).
startCommand = "pnpm --filter @stackfast/api start"

# /health is the unauthenticated liveness endpoint exposed by
# apps/api/src/app.ts; it is exempt from rate limiting (R4.9) so the
# Railway edge probe never consumes a generation/read token.
healthcheckPath = "/health"

# Upper bound (seconds) Railway waits for the first successful /health
# response before marking the deploy failed. The Node cold start
# (pnpm resolve + `node dist/index.js`) finishes in a few seconds on
# Railway's default container; 30 s leaves headroom without letting a
# genuinely broken deploy linger. Tune upward if cold starts regress.
healthcheckTimeout = 30

# If the process crashes post-boot, restart up to 3 times before the
# deployment is marked failed. ON_FAILURE matches design § "Rollback,
# observability, and runbook notes" — we do not want ALWAYS-retrying a
# crash loop to mask a real regression from the rollback operator.
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 3
Loading
Loading