Monorepo containing the application stack: an Astro frontend, a Directus (Docker) backend, and shared internal packages (UI kit, utilities, schemas, and configuration). Built with Turborepo + pnpm workspaces.
Node >= 22 and pnpm >= 10 are required
.
├── apps
│ ├── backend # Directus + Postgres + Redis via docker-compose
│ └── frontend # Astro site (React integrations, Tailwind via @tailwindcss/vite)
├── packages
│ ├── configs # Centralized ESLint (flat config) & future shared config exports
│ ├── directus # SDK layer: API helpers & schema exports around @directus/sdk
│ ├── shared # Cross-runtime utilities (Only, Try, cn, etc.)
│ └── ui # Reusable UI primitives (Radix, Tailwind, CVA)
├── turbo.json # Turborepo task pipeline configuration
├── pnpm-workspace.yaml # Workspace + dependency catalog (ensures single versions)
└── prettier.config.js # Formatting rules (Astro + Tailwind plugins)
| Package | Description | Notable Exports |
|---|---|---|
@stack/shared |
Lightweight utilities shared across apps | utils/only, utils/try, utils/ui |
@stack/ui |
Design system / component primitives | components/ui/*, Tailwind base styles |
@stack/directus |
Directus schema + API client composition | api/*, schemas/* |
@stack/configs |
ESLint flat config & future shared config points | eslint.config.js |
// Try helper (returns Data<T> | Error)
import { Try } from '@stack/shared/utils';
const { data, error } = await Try(() => fetchSomething());
// Only helper (conditionally run code client/server)
import { Only } from '@stack/shared/utils';
Only('client', () => console.log('Runs only in browser'));
// cn (merge class names + Tailwind dedupe)
import { cn } from '@stack/shared/utils';
const classes = cn('p-2', conditional && 'opacity-50');- Turborepo (task orchestration & caching)
- pnpm (workspaces + catalog pinned versions)
- Astro + React + Tailwind CSS
- Radix UI + class-variance-authority for composable components
- Directus (headless CMS / data layer) with Postgres + Redis
- TypeScript 5.8
- ESLint (flat config) + Prettier (Astro + Tailwind plugins)
| Script | Purpose |
|---|---|
pnpm dev |
Run all dev tasks (non-cached) across filtered packages/apps |
pnpm build |
Build all (respects dependency graph) |
pnpm lint |
Lint all (with --fix) |
pnpm format |
Prettier format supported files |
pnpm check-types |
Typecheck all packages/apps |
You can target a single workspace with Turborepo filters, e.g.:
pnpm build --filter frontend
pnpm lint --filter directus
pnpm dev --filter backendpnpm dev --filter frontendEnvironment variables consumed by Astro are declared via envField in astro.config.ts (e.g. PUBLIC_URL). Add a .env or .env.local in apps/frontend as needed.
The backend uses docker-compose inside apps/backend to orchestrate:
- Postgres (postgis image)
- Redis
- Directus (official image)
Create an .env file in apps/backend (values are examples):
DIRECTUS_PORT=8055
DIRECTUS_SECRET=replace_me
DB_USER=directus
DB_PASSWORD=directus
DB_DATABASE=directus
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=change_me
PUBLIC_URL=http://localhost:8055
WEBSOCKETS_ENABLED=true
CACHE_ENABLED=true
CACHE_AUTO_PURGE=true
# CORS
CORS_ENABLED=true
CORS_ORIGIN=http://localhost:3000,http://localhost:4321Then start services:
pnpm dev --filter backendThe Directus UI will be available at http://localhost:8055 (assuming default port mapping).
./uploads and ./extensions are volume-mounted; changes persist across container restarts.
- Create a folder under
packages/your-package. - Add a
package.jsonwithname,version,type, and scripts (lint,check-types). - Reference internal dependencies using
workspace:*. - Export public entrypoints through the
exportsfield. - (Optional) Add build / type tasks if the package produces artifacts.
Update imports elsewhere using the new package name (e.g. @stack/your-package).
- Flat ESLint config shared via
@stack/configs. - Prettier enforced via
pnpm format(Tailwind plugin sorts class names logically). - Type safety: run
pnpm check-typesor rely on IDE incremental checks.
turbo.json defines pipelines:
builddepends on upstream builds (^build), caches.dist/**(excluding Astro internals)devis markedpersistentand not cached (ideal for watch servers)lint/check-typesrun independently
Use filters to narrow scope:
pnpm turbo run build --filter=@stack/ui