# 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 `payload` schema in Postgres. - **Supabase** — trimmed self-hosted stack: `db`, `studio`, `meta`, `kong`, `auth`, `rest`, `storage`, `imgproxy`. - **packages/typescript-config** — shared `tsconfig` bases. [portless]: https://portless.sh --- ## 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 ```bash # 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. ```bash 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:3000` so > Astro's server-side `fetch` to 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), run `pnpm --filter cms dev:portless` and set > `NODE_EXTRA_CA_CERTS` to 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: ```bash 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.envDir` pointed 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_URL` accordingly. - 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](https://supabase.com/docs/guides/self-hosting/docker). --- ## Troubleshooting - **Astro homepage shows "Couldn't reach the CMS"** — Payload isn't running or the DB is down. Ensure `docker compose up -d` is healthy and `pnpm dev` shows the `cms` server on `:3000`. - **Payload can't connect to Postgres** — the DB exposes `:5432`; confirm `docker compose ps` shows `cdrdpyj-db` healthy and `DATABASE_URI` matches. - **Ports already in use** — `5432`, `8000`, `3000`, `4321` must be free (or change them in `.env` / the app dev scripts). ```