Files
shiftcraft/README.md
2026-04-17 23:26:01 +00:00

9.1 KiB

Nuxt PocketBase Starter

A full-stack starter template for rapidly building and shipping apps with Nuxt 4, PocketBase, and Capacitor (Android/iOS).

What's Included

  • Frontend — Nuxt 4 SPA with Vue 3, Nuxt UI, Tailwind CSS v4, Pinia state management
  • Backend — PocketBase (SQLite, REST API, Realtime WebSocket), Dockerized with migrations and hooks
  • Authentication — Email OTP, Google OAuth, Apple OAuth (auto-creates users on first login)
  • Push Notifications — FCM via Deno sidecar service, works on Android and web
  • Mobile — Capacitor for Android and iOS with deep linking support
  • i18n — English and German, frontend and backend (localized email templates)
  • Dev Tooling — ESLint, TypeScript, Vitest, Playwright, Mailpit (dev email viewer)

Prerequisites

Quick Start

1. Clone and install

git clone <repository-url>
cd nuxt-pocketbase-starter
pnpm install

2. Configure environment

cp .env.example .env

Edit .env to set your values. The defaults work for local development — you only need to configure OAuth credentials and Firebase if you want those features.

3. Start the backend

pnpm pocketbase:start

This starts three Docker containers:

Service URL Purpose
PocketBase http://localhost:8090 API + Admin UI (/_/)
Sidecar http://localhost:8091 FCM push notification delivery
Mailpit http://localhost:8025 Dev email viewer (catches all outgoing mail)

4. Start the frontend

In a second terminal:

pnpm dev

The app is available at http://localhost:3000.

Available Commands

Command Description
pnpm dev Start Nuxt dev server
pnpm build Production build
pnpm preview Preview production build locally
pnpm lint Run ESLint
pnpm typecheck Run TypeScript type checking
pnpm test Run tests (Vitest)
pnpm pocketbase:start Start PocketBase + sidecar + Mailpit (Docker)
pnpm pocketbase:stop Stop all backend containers
pnpm pocketbase:types Regenerate TypeScript types from PocketBase schema
pnpm assets Generate mobile app icons and splash screens

Environment Variables

See .env.example for all variables. Key ones:

Variable Description
APP_NAME Application display name
APP_ID App identifier (e.g., com.example.myapp)
NUXT_PUBLIC_APP_URL Frontend URL (default: http://localhost:3000)
NUXT_PUBLIC_POCKETBASE_URL PocketBase API URL (default: http://127.0.0.1:8090)
SUPERUSER_EMAIL / SUPERUSER_PW PocketBase admin credentials
AUTH_GOOGLE_CLIENT_ID / SECRET Google OAuth credentials
AUTH_APPLE_CLIENT_ID / SECRET Apple OAuth credentials
SIDECAR_SECRET Shared secret between PocketBase and sidecar
GOOGLE_CREDENTIALS_JSON Firebase service account JSON (for FCM push)

Project Structure

├── app/                        # Nuxt frontend
│   ├── pages/                  # File-based routes
│   ├── stores/                 # Pinia stores (user, counter, notifications, avatar)
│   ├── components/             # Vue components (App/, Counter/, Profile/)
│   ├── composables/            # usePocketBase()
│   ├── plugins/                # PocketBase client init
│   ├── middleware/             # Auth guard, user fetch
│   ├── i18n/locales/           # Frontend translations (en, de)
│   └── types/                  # TypeScript types
├── pocketbase/
│   ├── pb_migrations/          # Database migrations (run on startup)
│   ├── pb_hooks/               # Server-side hooks and custom endpoints
│   │   ├── templates/          # Localized HTML email templates
│   │   └── locales/            # Backend translations
│   ├── sidecar/                # Deno service for FCM push delivery
│   ├── docker-compose.yml
│   └── Dockerfile
├── android/                    # Capacitor Android project
├── ios/                        # Capacitor iOS project
├── Dockerfile                  # Nuxt production image
└── capacitor.config.ts         # Mobile app configuration

Customizing the Template

Rename the app

  1. Update APP_NAME and APP_ID in .env
  2. Update package.json name and version
  3. For mobile: regenerate assets with pnpm assets

Add a new backend collection

  1. Create a migration file in pocketbase/pb_migrations/ (next number in sequence, e.g., 5_my_table.js)
  2. Restart PocketBase: pnpm pocketbase:stop && pnpm pocketbase:start
  3. Regenerate types: pnpm pocketbase:types
  4. Create a Pinia store in app/stores/ using the generated types

Add a new language

  1. Add translation file to app/i18n/locales/ (e.g., fr.json)
  2. Register the locale in nuxt.config.ts under i18n.locales
  3. Add backend translations in pocketbase/pb_hooks/locales/ (e.g., fr.json)
  4. Add email templates in pocketbase/pb_hooks/templates/fr/
  5. Update LanguageCode type in app/types/i18n.types.ts

Add a custom API endpoint

Create a hook file in pocketbase/pb_hooks/ (e.g., myEndpoint.pb.js) using routerAdd(). See resetCounter.pb.js for an example. Hooks use PocketBase's JSVM — not Node.js.

Mobile Development

Android

pnpm build
npx cap sync android
npx cap open android     # Opens Android Studio

For push notifications, place your google-services.json at creds/fcm-google-services.json — it gets copied to the Android project automatically.

iOS

pnpm build
npx cap sync ios
npx cap open ios         # Opens Xcode

Requires a Mac with Xcode and an Apple Developer account.

CI/CD

Gitea Actions workflows in .gitea/workflows/ trigger on pushes to the release branch (and manual workflow_dispatch):

Workflow What it does
frontend.yaml Builds the Nuxt Docker image and pushes it to the Gitea Container Registry
backend.yaml Builds the PocketBase Docker image and pushes it to the Gitea Container Registry
android.yaml Builds the Android app via Fastlane and releases to Google Play
ios.yaml Builds the iOS app via Fastlane and uploads to App Store Connect

Required Gitea configuration

Repository variables (Settings → Actions → Variables):

Variable Example
NUXT_PUBLIC_APP_URL https://your-domain.com
NUXT_PUBLIC_POCKETBASE_URL https://pb.your-domain.com

Repository secrets (Settings → Actions → Secrets):

Secret Purpose
REGISTRY_TOKEN Gitea personal access token with package:write scope
PLAY_STORE_JSON_KEY_DATA Google Play service account JSON (Android release)
APP_STORE_CONNECT_API_KEY_ID App Store Connect API key ID (iOS release)
APP_STORE_CONNECT_API_ISSUER_ID App Store Connect issuer ID
APP_STORE_CONNECT_API_KEY_CONTENT Base64-encoded App Store Connect .p8 key
IOS_CERTIFICATE_BASE64 Base64-encoded .p12 signing certificate
IOS_CERTIFICATE_PASSWORD Password for the signing certificate
IOS_PROVISIONING_PROFILE_BASE64 Base64-encoded provisioning profile

Image tagging

Images are pushed to the Gitea Container Registry with two tags:

  • <registry>/<owner>/<repo>-frontend:<version> (from package.json)
  • <registry>/<owner>/<repo>-frontend:latest
  • <registry>/<owner>/<repo>-backend:<version> (from package.json)
  • <registry>/<owner>/<repo>-backend:latest

Deployment

Both the frontend and backend are published as Docker images via CI. Deploy them together with Docker Compose on any server.

Production Docker Compose

Create a docker-compose.yml on your server:

services:
  frontend:
    image: git.your-domain.com/owner/repo-frontend:latest  # Nuxt image from CI
    ports:
      - 3000:3000
    restart: unless-stopped

  pocketbase:
    image: git.your-domain.com/owner/repo-backend:latest  # PocketBase image from CI
    ports:
      - 8090:8090
      - 8091:8091
    volumes:
      - pb_data:/pb/pb_data
    env_file:
      - .env
    restart: unless-stopped

volumes:
  pb_data:

Place a production .env next to it with your secrets (SUPERUSER_EMAIL, SUPERUSER_PW, SIDECAR_SECRET, OAuth credentials, GOOGLE_CREDENTIALS_JSON, etc.).

docker compose pull && docker compose up -d

Put a reverse proxy (Caddy, Traefik, nginx) in front to handle TLS and route traffic to port 3000 (frontend) and 8090 (PocketBase API).

Building images manually

If you're not using CI, you can build locally:

# Frontend
docker build \
  --build-arg NUXT_PUBLIC_APP_URL=https://your-domain.com \
  --build-arg NUXT_PUBLIC_POCKETBASE_URL=https://pb.your-domain.com \
  -t nuxt-app .

# Backend
cd pocketbase
docker build --target prod -t pocketbase-app .

License

MIT