Skip to content

Deployment

This is the end-to-end procedure to bring the whole stack up from nothing. It uses Compose (Docker or Podman) and the *.wsw.local reverse-proxy hostnames. Each step notes the screenshot that will illustrate it once the UI captures are taken — the backlog lives in apps/docs/SCREENSHOTS.md.

  • Docker or Podman with Compose.
  • A reverse proxy that reads VIRTUAL_HOST (e.g. nginx-proxy) attached to an external network named proxy-network.
  • A Cloudflare R2 bucket (or any S3-compatible bucket) and its credentials.
  • Bun 1.3+ if you want to run workspace scripts on the host.

Step 1 — Create the external proxy network

Section titled “Step 1 — Create the external proxy network”

The Compose file declares proxy-network as external: true, so it must exist first:

Terminal window
docker network create proxy-network # or: podman network create proxy-network

Step 2 — Point *.wsw.local at your machine

Section titled “Step 2 — Point *.wsw.local at your machine”

Add the proxy hostnames to your hosts file (or a local DNS wildcard) so the browser resolves them to the reverse proxy:

/etc/hosts
127.0.0.1 api.wsw.local portal.wsw.local demo.wsw.local demo-api.wsw.local
127.0.0.1 adminer.wsw.local mailpit.wsw.local

Each app reads its own .env. The critical, easy-to-miss alignment is the integration key shared between the wrapper and the demo backend.

apps/api/.env (essentials):

Terminal window
DATABASE_URL=postgres://wccloud:wccloud@postgres:5432/webcloster
JWT_SECRET=<at-least-32-characters-of-secret>
REDIS_URL=redis://redis:6379
R2_ACCOUNT_ID=...
R2_ACCESS_KEY_ID=...
R2_SECRET_ACCESS_KEY=...
R2_BUCKET_NAME=...
R2_PUBLIC_URL=https://<public-bucket-domain> # optional, for public files
SCOPE_ENFORCEMENT=enforce

apps/demo-api/.env:

Terminal window
INTEGRATION_API_KEY=<the demo integration key>

Root shell / .env (consumed by the api service to seed the demo integration):

Terminal window
DEMO_INTEGRATION_API_KEY=<the SAME value as demo-api's INTEGRATION_API_KEY>
  • Create the bucket named in R2_BUCKET_NAME.
  • Create an R2 API token with object read/write on that bucket; use its values for R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY / R2_ACCOUNT_ID.
  • For public files, expose a public bucket domain and set R2_PUBLIC_URL. Leave it unset for a private-only bucket (private files are served via signed URLs).
Terminal window
docker compose up -d # or: podman compose up -d
docker compose ps # verify every service is running/healthy

The api service auto-runs db:migrate then seed:demo on boot; demo-api auto-runs seed:content. Both seed steps are idempotent and fault-tolerant.

A healthy stack looks like this (compose ps):

NAME STATUS
postgres Up (healthy)
redis Up
api Up
worker Up
portal Up
demo Up
demo-api Up
demo-mysql Up (healthy)
nginx-proxy Up

If you want to run them explicitly (or re-check after a schema change):

Terminal window
docker compose exec -T api sh -c "cd /app/apps/api && bun run db:migrate"

Expect the 0000–0008 chain applied (see the migration chain).

Using 'pg' driver for database querying
[✓] migrations applied successfully!
Terminal window
docker compose exec -T api sh -c "cd /app/apps/api && SUPER_ADMIN_EMAIL=you@example.com SUPER_ADMIN_PASSWORD='a-strong-password' bun run seed:admin"

This is idempotent — re-running reconciles the admin account.

Terminal window
curl -s http://api.wsw.local/api/v1/health
# → { "status": "ok", ... }

A healthy API returns:

{ "status": "ok", "env": "development", "timestamp": "…", "checks": { "db": "ok" } }

Open http://portal.wsw.local and sign in as the super admin from Step 7. From here you create a project, mint its PROJECT_API_KEY (shown once), and configure integrations, plans, and users.

The portal login screen at portal.wsw.local.

After signing in, the project dashboard shows the active session, quick links, and the project’s API keys:

The portal project dashboard after login — active session, quick links (Plans / Users / Integrations), and project API keys.

Step 10 — Create an integration with scopes

Section titled “Step 10 — Create an integration with scopes”

Under a project, create an integration and declare its scopes (remember SCOPE_ENFORCEMENT=enforce). The INTEGRATION_API_KEY is revealed once — store it in the consuming app’s environment.

The portal “Add integration” form — name, description, and the dev-mode toggle.

Step 11 — Verify the Rutas (spaces) surface

Section titled “Step 11 — Verify the Rutas (spaces) surface”

Open a user’s detail page in the portal. The Spaces (Rutas) section lets the Master create, edit (scopes + sub-limit + active), rotate the password, and delete Rutas.

The portal user-detail page: overview, storage, and the “Spaces (Rutas)” section listing Ruta Sur (files) and Ruta Norte (files/write/delete) with per-Ruta storage and rotate/edit/delete actions.

Step 12 — Exercise the demo (space login + isolation)

Section titled “Step 12 — Exercise the demo (space login + isolation)”

Open http://demo.wsw.local, then sign into a Ruta to confirm isolation: the space view lists only that Ruta’s files, not the Master-only files.

The demo Master dashboard after signing in as lucas2@lucas.com:

The demo Master dashboard — profile, storage quota, and the sidebar (Dashboard / Products / Rutas / Compression / Catalog).

The Master’s Rutas view — manage each space’s permissions and per-space storage, and filter files by Ruta:

The demo Master’s Rutas view: two spaces with permissions and storage, a Create Ruta action, and a per-Ruta file filter (All / Master only / Ruta Sur / Ruta Norte).

Signing into a Ruta confirms isolation — the space view lists only that Ruta’s files:

The demo space “Your files” view after a Ruta login — only the space’s own files are listed.

Step 13 — Confirm the live API reference

Section titled “Step 13 — Confirm the live API reference”

Open http://api.wsw.local/api/v1/docs for the Scalar UI over the live OpenAPI spec. See the API reference.

The Scalar UI rendering the live OpenAPI 3.1 spec at /api/v1/docs.

For password-reset flows in dev, set the API’s SMTP_HOST=mailtip, SMTP_PORT=1025 and read captured mail at http://mailpit.wsw.local (UI auth admin:password).

  • Demo returns 401 on /project/* — the DEMO_INTEGRATION_API_KEY ↔ demo-api INTEGRATION_API_KEY alignment is broken (Step 3). Re-align and re-run seed:demo.
  • 403 Missing required scope — the integration or token lacks a scope. Under enforce, declare it on the integration. See the integration guide.
  • Cross-site cookie dropped (401 in the demo) — call the API on the same site as the page (demo.wsw.local ↔ demo-api.wsw.local), not localhost.
  • proxy-network not found — create it first (Step 1).