|
|
||
|---|---|---|
| .claude | ||
| apps | ||
| packages/typescript-config | ||
| supabase/volumes | ||
| .env.example | ||
| .gitignore | ||
| .npmrc | ||
| README.md | ||
| docker-compose.yml | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| turbo.json | ||
README.md
cdrdpyj
A TypeScript Turborepo wiring an Astro frontend to a Payload CMS
backend, both running against a self-hosted Supabase stack (Postgres),
orchestrated from a single docker-compose file — with portless for
stable *.localhost dev URLs.
┌──────────────┐ REST /api ┌──────────────┐ SQL ┌───────────────────────────┐
│ Astro (web) │ ──────────────────────▶ │ Payload (cms)│ ─────────────────▶ │ Supabase (Postgres 17) │
│ SSR, :4321 │ │ Next, :3000 │ schema: payload │ db · studio · kong · │
│ web.localhost│ │ │ │ auth · rest · storage │
└──────────────┘ └──────────────┘ └───────────────────────────┘
▲ portless Studio dashboard :8000
- apps/web — Astro 5 (SSR via
@astrojs/node). Reads posts from Payload's REST API. - apps/cms — Payload 3 (on Next.js 16). Stores content in a dedicated
payloadschema in Postgres. - Supabase — trimmed self-hosted stack:
db,studio,meta,kong,auth,rest,storage,imgproxy. - packages/typescript-config — shared
tsconfigbases.
Prerequisites
| Tool | Version | Notes |
|---|---|---|
| Node | ≥ 24 | portless and Payload both require Node 24+ |
| pnpm | ≥ 9 | corepack enable or npm i -g pnpm |
| Docker | + Compose v2 | for the Supabase backend |
| portless | latest | pnpm add -g portless (optional but recommended) |
Quick start
# 1. Configure — one env file drives the whole stack
cp .env.example .env
# 2. Install workspace deps
pnpm install
# 3. Start the Supabase backend (Postgres + Studio + Auth + REST + Storage)
docker compose up -d # first run pulls images; give it a minute
# 4. Run the apps (Astro + Payload) with hot reload
pnpm dev
Then open:
| What | URL |
|---|---|
| Astro frontend | http://localhost:4321 (or the portless URL below) |
| Payload admin | http://localhost:3000/admin |
| Supabase Studio | http://localhost:8000 (login: supabase / this_password_is_insecure_and_should_be_updated) |
| Postgres | postgresql://postgres:postgres@localhost:5432/postgres |
First time in Payload admin you'll create the initial admin user. Then add a
Post (give it a slug) and it appears on the Astro homepage.
portless — stable *.localhost URLs
portless gives the frontend a memorable HTTPS URL instead of a port number,
and injects $PORT automatically.
pnpm add -g portless # once
pnpm dev # Astro is started via `portless web astro dev`
The web app is served at https://web.localhost. On first run portless
generates and trusts a local CA and binds to :443 (may prompt for sudo).
Why only the frontend? Payload stays on plain
http://localhost:3000so Astro's server-sidefetchto it uses plain HTTP — Node doesn't trust portless's local CA for server-to-server HTTPS. To also route Payload through portless (https://cms.localhost), runpnpm --filter cms dev:portlessand setNODE_EXTRA_CA_CERTSto portless's CA so Astro can reach it over HTTPS.
Prefer no portless? Use pnpm --filter web dev:plain (plain astro dev).
Everything in Docker (optional)
The apps also ship with Dockerfiles behind a compose apps profile:
docker compose --profile apps up -d --build
This runs Astro (:4321) and Payload (:3000) as containers alongside
Supabase. DATABASE_URI and the Astro→Payload URL are rewired to the internal
Docker network automatically. The recommended day-to-day loop is still
docker compose up -d (backend) + pnpm dev (apps) for hot reload.
Scripts
Run from the repo root:
| Command | Does |
|---|---|
pnpm dev |
Turbo runs both apps (Astro via portless, Payload plain) |
pnpm build |
Build all apps |
pnpm typecheck |
Type-check all apps |
pnpm supabase:up / :down |
Start / stop the Supabase backend |
pnpm supabase:logs |
Tail backend logs |
pnpm supabase:reset |
⚠️ Stop and delete all Postgres data + volumes |
pnpm stack:up / :down |
Backend and apps in Docker (apps profile) |
Per-app: pnpm --filter cms generate:types, pnpm --filter cms generate:importmap.
How configuration works
There is one .env at the repo root, shared by everything:
- docker-compose reads it natively.
- Payload loads it from the root in
apps/cms/src/payload.config.ts. - Astro loads it via
vite.envDirpointed at the root;PUBLIC_*vars are exposed to the browser.
The Supabase ANON_KEY / SERVICE_ROLE_KEY in .env.example are the standard
Supabase demo keys, signed with the matching demo JWT_SECRET. They work out of
the box for local dev. Changing JWT_SECRET invalidates them — regenerate
with Supabase's generate-keys steps if you do.
Project structure
.
├── docker-compose.yml # single-file Supabase stack (+ apps profile)
├── .env.example # one source of truth for all config
├── turbo.json # task pipeline
├── apps/
│ ├── web/ # Astro SSR frontend
│ └── cms/ # Payload 3 (Next.js) CMS
├── packages/
│ └── typescript-config/ # shared tsconfig bases
└── supabase/
└── volumes/ # kong config + Postgres init SQL (roles, jwt, payload schema)
Production notes
This scaffold is tuned for local development. Before deploying anywhere real:
- Regenerate every secret in
.env(POSTGRES_PASSWORD,JWT_SECRET+ matching keys,PAYLOAD_SECRET,DASHBOARD_PASSWORD,PG_META_CRYPTO_KEY, the S3 protocol keys). - Put Kong / the apps behind TLS and real domains; set
SITE_URL,API_EXTERNAL_URL,SUPABASE_PUBLIC_URL,PAYLOAD_PUBLIC_SERVER_URL,PUBLIC_PAYLOAD_API_URLaccordingly. - Switch Payload from dev "push" to committed migrations
(
pnpm --filter cms payload migrate:create). - Consider adding back the Supabase services this scaffold trims (Realtime, Edge Functions, Supavisor pooler) if you need them — see Supabase self-hosting docs.
Troubleshooting
- Astro homepage shows "Couldn't reach the CMS" — Payload isn't running or
the DB is down. Ensure
docker compose up -dis healthy andpnpm devshows thecmsserver on:3000. - Payload can't connect to Postgres — the DB exposes
:5432; confirmdocker compose psshowscdrdpyj-dbhealthy andDATABASE_URImatches. - Ports already in use —
5432,8000,3000,4321must be free (or change them in.env/ the app dev scripts).