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
85 changes: 18 additions & 67 deletions .env.local.example
Original file line number Diff line number Diff line change
@@ -1,82 +1,33 @@
# Copy to `.env.local` at the repository root. Never commit `.env.local`.
# Prisma scripts load this file via `dotenv -e ../.env.local` from `api/`.
# The API loads `.env.local` when NODE_ENV is not production.

# -----------------------------------------------------------------------------
# Recommended: **localhost frontend only** — API + data on AWS (ECS, RDS, S3, SQS)
# -----------------------------------------------------------------------------
# Do not run `pnpm dev:api` for this workflow.
#
# 1) Copy `frontend/.env.local.example` → `frontend/.env.local`
# 2) Set `VITE_API_URL` to `http://<ECS task public IP>:3001` (see example file for where to find the IP).
# 3) Ensure the ECS `cardboardforge-prod-api` service has desired count ≥ 1 and tasks are running.
# 4) Run: `pnpm dev:frontend` and open http://localhost:5173
#
# No AWS keys in the browser — only the API URL. The API task uses IAM roles for S3/SQS/RDS.

# -----------------------------------------------------------------------------
# Local UI + API against **production** AWS (S3, SQS, RDS)
# -----------------------------------------------------------------------------
# 1) IAM: use an access key for a user/role that can reach S3 buckets, SQS queue,
# and (for Prisma) RDS. Same credentials you use with AWS CLI are fine if scoped.
#
# 2) RDS from your laptop: the managed DB is in a VPC. Allow your public IP:
# - Get IP: curl -s https://checkip.amazonaws.com
# - Set rds_dev_access_cidrs without committing IPs, e.g. before apply:
# export TF_VAR_rds_dev_access_cidrs='["YOUR.HOME.IP/32","YOUR.SCHOOL.IP/32"]'
# or copy infra/envs/prod/rds.auto.tfvars.example → rds.auto.tfvars (gitignored).
# - Run: cd infra/envs/prod && terraform apply
# That opens Postgres to your IP and sets the instance to publicly addressable.
# Fully local dev: Docker Postgres + LocalStack (no real AWS).
#
# 3) DATABASE_URL: postgresql://forge:PASSWORD@HOST:5432/cardboardforge
# Password: cd infra/envs/prod && terraform output -raw rds_master_password
# Host: terraform output -raw rds_endpoint (hostname only, no :5432 in host)
# Used by: `pnpm dev:local`, `pnpm dev:api`, `pnpm migrate:deploy`, and the worker (default).
#
# 4) Buckets + queue: terraform output assets_bucket_name exports_bucket_name pdf_queue_url
#
# 5) **Do not set AWS_ENDPOINT_URL** — leave unset so the SDK uses real AWS.
#
# 6) ECS worker vs local worker: if the prod ECS worker service is running, it shares
# the same SQS queue. For predictable local debugging, scale ECS worker desired count
# to 0 in Terraform or the console, then run the Python worker on your machine with
# the same env (real AWS, no endpoint URL).
# 1) cp .env.local.example .env.local
# 2) pnpm docker:up
# 3) pnpm migrate:deploy
# 4) pnpm dev:local

NODE_ENV=development
PORT=3001
LOG_LEVEL=debug

AWS_REGION=us-east-1
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# AWS_SESSION_TOKEN= # if using temporary credentials / SSO export
# Optional: fail fast if values look like prod AWS/RDS (see api/src/lib/devProfile.ts)
CARDGOOSE_DEV_PROFILE=fully-local

# Leave commented for production AWS:
# AWS_ENDPOINT_URL=http://localhost:4566
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=test
AWS_SECRET_ACCESS_KEY=test
AWS_ENDPOINT_URL=http://localhost:4566

DATABASE_URL=postgresql://forge:REPLACE_WITH_RDS_PASSWORD@REPLACE_WITH_RDS_HOST:5432/cardboardforge
DATABASE_URL=postgresql://forge:forge@localhost:5433/cardgoose

S3_BUCKET_ASSETS=cardboardforge-prod-assets-xxxxxxxx
S3_BUCKET_EXPORTS=cardboardforge-prod-exports-xxxxxxxx
SQS_QUEUE_URL=https://sqs.us-east-1.amazonaws.com/123456789012/cardboardforge-prod-pdf-generation
S3_BUCKET_ASSETS=cardgoose-assets
S3_BUCKET_EXPORTS=cardgoose-exports
SQS_QUEUE_URL=http://localhost:4566/000000000000/cardgoose-pdf-generation

# Local dev only — tokens issued by your local API. Not the same as ECS JWT unless you copy it.
JWT_SECRET=change-me-local-dev-only

# PDF worker (Playwright) loads this origin + /render. MUST match Vite’s port (check terminal:
# "Local: http://localhost:5173/" — often 5174 if 5173 is already taken).
# RENDER_URL=http://localhost:5173

# Frontend (Vite): leave empty to use the dev proxy to the local API
# VITE_API_URL=
RENDER_URL=http://localhost:5173

# -----------------------------------------------------------------------------
# Alternative: full local stack (Docker Postgres + LocalStack)
# -----------------------------------------------------------------------------
# DATABASE_URL=postgresql://forge:forge@localhost:5433/cardgoose
# AWS_ACCESS_KEY_ID=test
# AWS_SECRET_ACCESS_KEY=test
# AWS_ENDPOINT_URL=http://localhost:4566
# S3_BUCKET_ASSETS=cardgoose-assets
# S3_BUCKET_EXPORTS=cardgoose-exports
# SQS_QUEUE_URL=http://localhost:4566/000000000000/cardgoose-pdf-generation
# Then: pnpm docker:up
# VITE_API_URL= # leave unset; Vite proxies to local API
24 changes: 11 additions & 13 deletions .env.production.example
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
# Production (AWS) — copy to `.env.production` at the repository root. Never commit `.env.production`.
# Use for: running Prisma against RDS from your laptop (`pnpm migrate:deploy:prod`),
# or local smoke tests against the live API. ECS tasks get env from Terraform, not this file.
# Copy to `.env.production` at the repository root. Never commit `.env.production`.
#
# Used only on your laptop for:
# - `pnpm migrate:deploy:prod` (Prisma migrations against RDS)
#
# Real ECS tasks get env from Terraform, not this file.

NODE_ENV=production

DATABASE_URL=postgresql://forge:YOUR_RDS_PASSWORD@YOUR_RDS_HOST:5432/cardboardforge

AWS_REGION=us-east-1
# IAM user or SSO profile credentials; leave endpoint empty for real AWS
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_ENDPOINT_URL=
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=

# From `terraform output` in infra/envs/prod
S3_BUCKET_ASSETS=cardboardforge-prod-assets-xxxxxxxx
S3_BUCKET_EXPORTS=cardboardforge-prod-exports-xxxxxxxx
SQS_QUEUE_URL=https://sqs.us-east-1.amazonaws.com/123456789012/cardboardforge-prod-pdf-generation

JWT_SECRET=use-a-long-random-string
JWT_SECRET=use-a-long-random-string-matching-prod-if-needed

PORT=3001
NODE_ENV=production

# Frontend pointed at ECS task public IP or future ALB
# VITE_API_URL=https://api.example.com
130 changes: 58 additions & 72 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,117 +27,104 @@ Designing board games is hard. It requires **rapid iteration** and the ability t

- Node.js 20+
- [pnpm](https://pnpm.io/) 9+
- Python 3.x + `boto3` for the worker (`pip install boto3` if needed)
- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) when using **hybrid** or **production** against real AWS
- Docker — required for **fully local** (Postgres + LocalStack) and useful for optional compose services
- Terraform 1.9+ — for **hybrid** (RDS access from your IP) and **production** (full AWS stack)
- Python 3.12+ recommended — for the PDF worker: `cd worker && python3 -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt && playwright install chromium`
- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) — for **production** operations (e.g. Prisma against RDS); not needed for fully local dev
- Docker — **fully local** dev (Postgres + LocalStack)
- Terraform 1.9+ — **production** infrastructure ([`infra/envs/prod`](infra/envs/prod))

---

## Development modes
## Local development (recommended)

Pick one path. They differ by **where Postgres and object/queue services run**, not by repo layout.


| Mode | Frontend | API & worker | Database | Buckets & queue |
| --------------- | -------- | ------------ | -------- | --------------------- |
| **Fully local** | Your Mac | Your Mac | Docker | LocalStack (emulated) |
| **Hybrid** | Your Mac | Your Mac | AWS RDS | AWS S3 & SQS |
| **Production** | ECS | ECS | AWS RDS | AWS S3 & SQS |
Develop against **Docker Postgres** and **LocalStack** (S3/SQS). No real AWS calls. When you are ready, **push to `main`** and [GitHub Actions](.github/workflows/ci.yml) builds and deploys to ECS.

| Piece | Where it runs locally |
| ----- | --------------------- |
| Frontend + API + worker | Your machine |
| Database | Docker Postgres (`localhost:5433`) |
| S3 / SQS | LocalStack (`localhost:4566`) |

The API container can serve the built SPA in production (`NODE_ENV=production`). Until you add a stable URL (ALB, CloudFront, etc.), you may use the task public IP for smoke tests.

### Fully local (Docker Postgres + LocalStack)

Use this when you want **no AWS calls**: everything emulated or on your machine.
### Setup

1. **Start backing services** (Postgres on host port **5433**, LocalStack on **4566**):
```bash
pnpm docker:up
```
Optional: `docker compose build` if you use the `api` / `worker` compose services.
2. **Configure** `[.env.local.example](.env.local.example)` → `**.env.local`** at the repo root using the **“full local stack”** block: `DATABASE_URL` pointing at `localhost:5433`, `AWS_ENDPOINT_URL=http://localhost:4566`, and bucket/queue names `**cardgoose-*`** (they must match `[docker-compose.yml](docker-compose.yml)` and `[docker/localstack-ready.d/init-aws.sh](docker/localstack-ready.d/init-aws.sh)`).
3. **Migrations:**
```bash
pnpm migrate:deploy
```
4. **Run three processes** (three terminals from the repo root):
```bash
pnpm dev:api
pnpm dev:frontend
```
5. Open the URL Vite prints (often `http://localhost:5173`).
1. **One-time:** copy [`.env.local.example`](.env.local.example) to **`.env.local`** at the repo root. It sets LocalStack (`AWS_ENDPOINT_URL`), Docker Postgres on **5433**, and `cardgoose-*` buckets/queue names that match [`docker-compose.yml`](docker-compose.yml) and [`docker/localstack-ready.d/init-aws.sh`](docker/localstack-ready.d/init-aws.sh). Optional `CARDGOOSE_DEV_PROFILE=fully-local` makes the API exit on startup if values look like real AWS/RDS by mistake.

Do **not** set `VITE_API_URL` in `frontend/.env.local` if the Vite dev server should proxy `/api` and `/health` to `http://localhost:3001` (see `[frontend/vite.config.ts](frontend/vite.config.ts)`).
2. **Start the app** (Postgres + LocalStack, then API + Vite):

**PDF exports:** set `**RENDER_URL`** in `.env.local` to the exact origin Vite prints (including port). If the worker runs in Docker and Vite on the host, use something like `http://host.docker.internal:5173`. Restart the worker after changes.
```bash
pnpm dev:local
```

---
Or start backing services only, then run processes yourself:

### Hybrid (local apps, real AWS)
```bash
pnpm docker:up
pnpm migrate:deploy
pnpm dev:api
pnpm dev:frontend
```

Use this for day-to-day work: **Vite, the API, and the worker on your laptop** with **real RDS, S3, and SQS** in `us-east-1`. You get hot reload without deploying containers.
`pnpm docker:up` and `pnpm docker:up:local` are equivalent (Postgres on **5433**, LocalStack on **4566**).

**1. Terraform (occasional)** — in `[infra/envs/prod](infra/envs/prod)`:
3. **First run / after schema changes:**

- Allow your laptop to reach RDS: set `rds_dev_access_cidrs` without committing real IPs — e.g. `export TF_VAR_rds_dev_access_cidrs='["YOUR.PUBLIC.IP/32"]'` before `terraform apply`, or copy `[infra/envs/prod/rds.auto.tfvars.example](infra/envs/prod/rds.auto.tfvars.example)` to `**rds.auto.tfvars`** (gitignored). Update when your IP changes.
- Set `ecs_desired_count = 0` for the **worker** service if you run the Python worker locally (avoids two consumers on the same SQS queue and idle Fargate cost).
- Run `terraform apply`.
```bash
pnpm migrate:deploy
```

**2. Root `.env.local`** — copy `[.env.local.example](.env.local.example)` and fill **real** values:
4. Open the URL Vite prints (often `http://localhost:5173`).

- `**DATABASE_URL`** — RDS host, user `forge`, password from `terraform output -raw rds_master_password`, database name from your RDS instance (by default Terraform uses the `project_name` value, e.g. `cardboardforge`).
- `**S3_BUCKET_ASSETS**`, `**S3_BUCKET_EXPORTS**`, `**SQS_QUEUE_URL**` — from `terraform output` (`assets_bucket_name`, `exports_bucket_name`, `pdf_queue_url`).
**PDF exports:** use `pnpm dev:local:worker` to run API + Vite + the Python worker together, or run the worker in another terminal (see [`worker/README.md`](worker/README.md)). Set `RENDER_URL` in `.env.local` to the exact origin Vite prints (including port). If the worker runs in Docker and Vite on the host, use something like `http://host.docker.internal:5173`.

Do **not** set `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_ENDPOINT_URL` for real AWS: use the default credential chain (`~/.aws/credentials` or SSO).
Do **not** set `VITE_API_URL` in `frontend/.env.local` when the dev server should proxy `/api` and `/health` to `http://localhost:3001` (see [`frontend/vite.config.ts`](frontend/vite.config.ts)).

**3. Frontend:** do **not** set `VITE_API_URL` in `frontend/.env.local` (or remove it) so the dev server proxies to the local API.
**Optional — UI only against a deployed API:** copy [`frontend/.env.local.example`](frontend/.env.local.example) to `frontend/.env.local`, set `VITE_API_URL` to your ECS task URL, run `pnpm dev:frontend`. You may need CORS configured on the deployed API for `http://localhost:5173`.

**4. Migrations:**
**Troubleshooting (local):** After a **Docker / LocalStack restart**, S3 buckets may be missing; the dev API **creates missing `S3_BUCKET_*` buckets** when `AWS_ENDPOINT_URL` points at LocalStack (**4566**). If you still see **`NoSuchBucket`**, seed manually, then restart `pnpm dev:local`:

```bash
pnpm migrate:deploy
aws --endpoint-url=http://localhost:4566 s3 mb s3://cardgoose-assets 2>/dev/null || true
aws --endpoint-url=http://localhost:4566 s3 mb s3://cardgoose-exports 2>/dev/null || true
aws --endpoint-url=http://localhost:4566 sqs create-queue --queue-name cardgoose-pdf-generation 2>/dev/null || true
```

**5. Run** `pnpm dev:api`, `pnpm dev:frontend`, and the worker as in the fully local section.
Or: `docker compose restart localstack` (from the repo root, with compose services up).

**Queue contention:** if the **ECS worker** is scaled up, it shares the same SQS URL as your laptop. For predictable local PDF debugging, keep ECS worker desired count at **0** while testing locally.
---

**Frontend-only against a cloud API:** set `VITE_API_URL=http://<ecs-task-public-ip>:3001` in `frontend/.env.local` and run `pnpm dev:frontend` only. Find the task IP in the ECS console (see `[frontend/.env.local.example](frontend/.env.local.example)`). This is still “hybrid” from the browser’s perspective (local UI, remote API).
## Production (AWS)

**Troubleshooting RDS:** if Prisma cannot connect, confirm your IP is in `rds_dev_access_cidrs`, re-apply Terraform, and that `DATABASE_URL` matches `terraform output -raw rds_endpoint`.
**Production** means the **API and worker run on ECS Fargate**, with **RDS, S3, and SQS** from Terraform. CI deploys on push to `main` (see [`.github/workflows/ci.yml`](.github/workflows/ci.yml)).

---
1. **Infrastructure** — follow [infra/BOOTSTRAP.md](infra/BOOTSTRAP.md) and apply [`infra/envs/prod`](infra/envs/prod).

### Production (deployed on AWS)
2. **Prisma against RDS (from your laptop)** — copy [`.env.production.example`](.env.production.example) to **`.env.production`** (never commit) with `DATABASE_URL` and AWS resource names matching Terraform outputs. Then:

**Production** means the **API and worker run on ECS Fargate**, with **RDS, S3, and SQS** provisioned by Terraform. CI can build and push images and force new deployments (see `[.github/workflows/ci.yml](.github/workflows/ci.yml)`).
```bash
pnpm migrate:deploy:prod
```

1. **Bootstrap and apply** — follow [infra/BOOTSTRAP.md](infra/BOOTSTRAP.md) for remote state, then apply `[infra/envs/prod](infra/envs/prod)`. Resource names, buckets, queues, and the RDS database name follow your Terraform variables (defaults in this repo still use the historical `cardboardforge` prefix for AWS resources).
2. **Runtime config** — ECS tasks receive env vars from Terraform (not from `.env.local`). For **one-off Prisma operations** against production RDS (migrations, introspection), copy `[.env.production.example](.env.production.example)` to `**.env.production`** (never commit) with values that match your live stack:
```bash
pnpm migrate:deploy:prod
```
3. **Deploys** — pushes to `main` run lint, tests, Terraform validate, Docker builds, and (with secrets configured) ECR push + ECS `update-service --force-new-deployment`. Adjust branch rules in the workflow if your default branch differs.
4. **Smoke checks** — hit `/health` on a running API task; confirm `service` identifies as `cardgoose-api`. Scale worker and API services in ECS per load and cost.
ECS tasks do **not** read `.env.production`; they get env from Terraform.

---
3. **Deploy** — push to `main`: lint, tests, Docker builds, ECR push, ECS rolling update. Run the same checks locally first: `pnpm run format:check`, `pnpm -r run lint`, `pnpm test:all`.

## Environment files
4. **Smoke** — `GET /health` on a running API task; `service` should be `cardgoose-api`. Confirm whether the API container runs migrations on startup so you do not apply twice ([`api/Dockerfile`](api/Dockerfile)).

---

| File | Purpose |
| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `[.env.local.example](.env.local.example)` | Template for **local** dev — copy to `**.env.local`** at the repo root. |
| `[.env.production.example](.env.production.example)` | Template for **production DB / ops** — copy to `**.env.production`** for Prisma and tooling (never commit). |
## Environment files

| File | Purpose |
| ---------------------------------------------------- | ------- |
| [`.env.local.example`](.env.local.example) | Template → **`.env.local`** — local dev (Docker Postgres + LocalStack). Used by `pnpm dev:local`, `pnpm dev:api`, `pnpm migrate:deploy`. |
| [`.env.production.example`](.env.production.example) | Template → **`.env.production`** — **only** for `pnpm migrate:deploy:prod` against RDS. |

The API loads `**.env.local**` when `NODE_ENV` is not `production`. Prisma CLI uses `dotenv -e ../.env.local` or `../.env.production` via the `prisma:deploy` scripts. The Baker worker loads the same `**.env.local**` when run locally (`python-dotenv`; existing environment variables win).
The API loads **`.env.local`** via `dotenv-cli` on `pnpm dev` scripts. The worker loads **`.env.local`** when run locally ([`worker/README.md`](worker/README.md)).

If you still have a single `**.env**` from an older setup, merge into `.env.local` and remove `.env`.
If you still have a root **`.env`** from an older setup, merge into `.env.local` and remove `.env`.

**Docker Compose API:** `docker compose up api` runs `prisma migrate deploy` before `node` (see `[docker-compose.yml](docker-compose.yml)`).
**Docker Compose API:** `docker compose up api` runs `prisma migrate deploy` before `node` (see [`docker-compose.yml`](docker-compose.yml)).

---

Expand All @@ -150,4 +137,3 @@ pnpm test:all
cd infra && terraform fmt -check -recursive
cd infra/envs/prod && terraform init -input=false && terraform validate
```

2 changes: 1 addition & 1 deletion api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ model CardGroup {
@@index([projectId, sortOrder])
}

/// react-konva stage JSON + metadata (hybrid schema from design doc).
/// react-konva stage JSON + metadata.
model Layout {
id String @id @default(uuid()) @db.Uuid
projectId String @map("project_id") @db.Uuid
Expand Down
Loading
Loading