5.6 KiB
Carpa — Turborepo
A multilingual content platform built as a Turborepo monorepo:
- Payload CMS backend on SQLite, multilingual (Spanish default + English, French, Portuguese)
- Typesense for faceted search, filtering and full-text search
- Astro frontend (Tailwind v4 + DaisyUI) — the first of three parallel frontends
- Shared design-system package (DaisyUI + React) documented in Storybook
Astro is live in this pass. Two more parallel frontends are planned on the same backend + Typesense foundation: Nuxt (NuxtUI) and Next.js (DaisyUI).
Layout
carpa_turborepo/
├── apps/
│ ├── cms/ # Payload 3 + Next.js 16, SQLite, es/en/fr/pt localization
│ └── web-astro/ # Astro 7 SSR, Tailwind v4 + DaisyUI, Typesense search
├── packages/
│ ├── typesense/ # shared client, collection schema, mappers + indexer CLI
│ ├── ui/ # shared DaisyUI/React design system + Storybook
│ ├── tailwind-config/# shared Carpa DaisyUI theme (carpa / carpa-dark)
│ ├── tsconfig/ # shared TS base configs
│ └── eslint-config/ # shared flat ESLint config
├── docker-compose.yml # Typesense service
├── turbo.json
└── pnpm-workspace.yaml
Prerequisites
- Node ≥ 20.9, pnpm 10 (
corepack enable) - Docker (for Typesense) — or a Typesense server binary
Quick start
# 1. Install
pnpm install
# 2. Environment — copy the examples and set secrets
cp .env.example .env
cp apps/cms/.env.example apps/cms/.env
cp apps/web-astro/.env.example apps/web-astro/.env
# In apps/cms/.env set a real PAYLOAD_SECRET: openssl rand -hex 32
# 3. Start Typesense
pnpm typesense:up
# 4. Start the CMS (creates the SQLite schema on first boot)
pnpm --filter @carpa/cms dev # http://localhost:3000/admin
# 5. (optional) Seed multilingual demo content
pnpm --filter @carpa/cms seed # admin@carpa.test / changeme123
# 6. Create the browser search-only key, then build the search index
pnpm typesense:keys
pnpm typesense:index
# 7. Start the Astro frontend
pnpm --filter @carpa/web-astro dev # http://localhost:4321 (redirects to /es/)
Or run everything at once with pnpm dev (Turborepo).
Pretty local URLs (Portless)
Instead of juggling localhost:3000 / :4321, you can serve the apps behind
stable, HTTPS .localhost names with Portless. It's
wired in as an overlay — each app stays pinned to its normal port
(portless.json), so the plain localhost workflow and the Typesense CLIs
keep working unchanged; Portless just adds nice URLs on top.
# From the repo root — starts the apps behind their URLs and trusts a local CA
pnpm portless
| URL | App |
|---|---|
| https://cms.carpa.localhost | Payload admin + REST |
| https://web.carpa.localhost | Astro frontend |
For the in-browser search to work over HTTPS (a browser blocks a page on
https://… from calling http://localhost:8108), expose Typesense over HTTPS
too and point the frontend at it:
pnpm portless:typesense # → https://typesense.carpa.localhost
then add the values from apps/web-astro/.env.portless.example to
apps/web-astro/.env and restart the Astro dev server. Server-side calls
(Astro SSR → Payload) can stay on http://localhost:3000.
First run trusts a local CA system-wide and may prompt for your password. Portless also injects that CA into the dev servers it starts, so their Node processes trust the HTTPS URLs automatically.
How the pieces connect
Localization. Locales live in one place — packages/typesense/src/config.ts
(LOCALES, DEFAULT_LOCALE). Payload's localization config, the search index,
and every frontend read from it, so adding a language is a one-line change.
Search sync. Content stays searchable two ways:
- Live — Payload
afterChange/afterDeletehooks (apps/cms/src/hooks/typesense.ts) upsert/remove documents across all locales as you edit. If Typesense is down the hooks log a warning and the CMS keeps working. - Bulk —
pnpm typesense:indexrebuilds the wholecarpa_contentcollection from Payload's REST API.
The Typesense collection is flat: one row per (document, locale), with locale,
type, categories and tags as facets. The Astro UI filters by the active locale
and refines on the rest.
Search-only key. The browser uses a scoped documents:search key, never the
admin key. pnpm typesense:keys provisions it deterministically.
Frontends
| App | Framework | UI library | Status |
|---|---|---|---|
web-astro |
Astro 7 (SSR) | DaisyUI | ✅ implemented |
web-nuxt |
Nuxt | NuxtUI | ⏳ planned |
web-next |
Next.js | DaisyUI | ⏳ planned |
All three consume the same Payload REST API and the same Typesense index.
Useful scripts (root)
| Script | What it does |
|---|---|
pnpm dev |
Run all apps via Turborepo |
pnpm build |
Build everything |
pnpm typecheck |
Typecheck every workspace |
pnpm typesense:up / :down |
Start/stop Typesense (Docker) |
pnpm typesense:keys |
Create the browser search-only key |
pnpm typesense:index |
Full reindex from Payload into Typesense |
pnpm ui storybook |
Run the design-system Storybook (port 6006) |
Notes
- SQLite → Turso/libSQL: the SQLite adapter uses a libSQL client, so pointing
DATABASE_URIat a Turso URL (with an auth token) is a drop-in for production. - Migrations: dev uses Drizzle
push(auto-sync). For production runpnpm --filter @carpa/cms migrate:createthenmigrate.