tests/{unit,integration,e2e}/, at the repo root (not under frontend/,
since e2e exercises the frontend and PocketBase together) — all
against real code rather than mocks:
- unit/: Vitest (nuxt environment) — pure functions in server/utils
and shared/utils, plus components/composables via
@nuxt/test-utils/runtime's mountSuspended.
- integration/: Vitest (node environment) — @nuxt/test-utils/e2e's
setup() builds and runs the real Nitro server against a real
ephemeral PocketBase instance, exercising the actual /content/* and
/admin routes end to end.
- e2e/: Playwright, a real browser against the built app and another
ephemeral PocketBase, covering the user-facing flows (home, blog
archive, article detail incl. JSON-LD, category filter, 404s, the
/admin redirect).
The Vitest/Playwright tooling (and node_modules) stays in frontend/,
the repo's only npm project; its configs just point at ../tests/.
Playwright resolves packages from the node_modules nearest each spec
file, so `npm run test:e2e` first symlinks tests/node_modules to
frontend/node_modules (pretest:e2e, idempotent, gitignored).
tests/support/pocketbase.ts is the shared piece behind integration
and e2e: it downloads the same pinned PocketBase binary
pocketbase/Dockerfile uses, starts it against the real
pocketbase/pb_migrations/ (not a copy), and seeds it from
tests/support/fixtures.ts — so these tests run against the exact
schema and API rules that ship to production, catching the class of
bug that only shows up when PocketBase actually enforces them (e.g. a
wrong filter/fields query string silently returning nothing, or too
much).
CLAUDE.md and docs/frontend.md document the new npm scripts and the
single-test commands for each layer.
Blog
Blog platform: a public website built with Nuxt, and a private PocketBase CMS where the articles are written. Everything runs behind Caddy via Docker Compose.
Browser → Caddy ─┬─ /_/*, /api/* → PocketBase (CMS)
└─ everything else → Nuxt (website), which also redirects /admin → /_/
There are no front-end accounts: only the superuser writes content. An article is a title, a Markdown body, a cover image and a category.
Development
No domain and no TLS needed — the services are published on localhost instead of going through Caddy. Requires Docker.
1. Create the environment file
cp .env.example .env
2. Set the admin credentials. Replace POCKETBASE_ADMIN_EMAIL and
POCKETBASE_ADMIN_PASSWORD in .env with your own — this is the superuser account
PocketBase creates (or updates) on every start. The domain and URL variables can stay as
they are for local use.
chmod 600 .env
3. Start the stack
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build pocketbase frontend
The first build takes a few minutes. docker-compose.dev.yml publishes the ports on
127.0.0.1 and leaves Caddy out; it must always be passed explicitly, so it can never be
picked up by accident in production.
4. Open the admin UI at http://localhost:3000/admin (redirects to PocketBase's dashboard
— this works in dev too, without Caddy, since the redirect is handled by the frontend itself)
and log in with the credentials from .env.
5. Write something. In the admin UI: create a categories record, then an
articles record (the content field is Markdown), then set publishedAt — the
website only shows articles whose publishedAt is set and not in the future.
6. Open the website at http://localhost:3000 — home, /blog, /blog/<slug> and
/category/<slug>.
Useful commands:
docker compose logs -f pocketbase # follow the CMS logs
docker compose -f docker-compose.yml -f docker-compose.dev.yml restart frontend
docker compose down # stop, keep the data
docker compose down -v # stop and WIPE the CMS database and media
To iterate on the frontend without rebuilding an image every time: cd frontend && npm run dev. It defaults to http://localhost:8090 for PocketBase, so it works against the
containerised CMS as is; override it with NUXT_POCKETBASE_URL if needed.
Production
1. Point the DNS at the server. One A record (and AAAA if you have IPv6) on the
public IP of the machine:
| Record | Purpose |
|---|---|
example.com |
the website, and, at /admin, the PocketBase admin UI |
Wait for the record to resolve before starting the stack — Caddy requests the certificate on the first boot and a failed challenge means a retry delay.
2. Open the firewall for ports 80 and 443 only. Port 80 is required: Caddy uses
it for the ACME challenge and to redirect to HTTPS. The admin UI shares port 443 with the
site (reachable by anyone who knows /admin, protected only by the superuser login, so
keep the password strong); PocketBase itself is never published directly, only Caddy's
proxy to it.
3. Configure the environment. Copy .env.example to .env on the server and set:
| Variable | Value |
|---|---|
PUBLIC_DOMAIN |
example.com |
ACME_EMAIL |
a mailbox you read — Let's Encrypt sends expiry warnings there |
PUBLIC_SITE_URL |
https://example.com |
PUBLIC_POCKETBASE_URL |
https://example.com — same origin, Caddy proxies /_/ and /api/ there |
POCKETBASE_URL |
leave it as http://pocketbase:8090 — internal address, never public |
POCKETBASE_ADMIN_EMAIL / POCKETBASE_ADMIN_PASSWORD |
your real superuser credentials |
Keep .env out of version control; it is already ignored.
Changing
POCKETBASE_ADMIN_PASSWORDlater and restarting rotates the superuser password immediately (the entrypoint upserts it on every boot) — a credential change, so treat it with the same care as any production credential rotation.
4. Start everything
docker compose up -d --build
This time Caddy is included: it serves both the website and, under /admin, the CMS
admin UI on PUBLIC_DOMAIN, obtains and renews the TLS certificate on its own, and adds
HSTS and the other security headers.
5. Log into the admin UI at https://example.com/admin with the credentials from
.env, then publish as in development.
6. Back up what is not in git: the pocketbase-data volume (database and uploaded
media together). Nothing else on the server holds state.
docker run --rm -v blog_pocketbase-data:/data -v "$PWD:/out" alpine tar czf /out/pocketbase-data.tar.gz -C /data .
Updating a running site: pull the new code, then docker compose up -d --build.
PocketBase applies new pb_migrations/ files at startup; take a backup first.
Architecture, conventions and constraints are documented in CLAUDE.md.
License
Proprietary — Copyright (c) 2026 Davide Grilli. All rights reserved. See LICENSE.