This document explains how to configure Tada for different deployment scenarios.
Tada supports three deployment modes:
| Mode | Use Case | Cloud Features |
|---|---|---|
| Development | Local development | Off |
| Self-Hosted | Your own server (Docker/CapRover) | Optional |
| Cloud | Multi-tenant SaaS deployment | On |
For local development on your machine or in a dev container.
All npm/bun scripts set DATABASE_URL explicitly in package.json:
{
"dev": "DATABASE_URL=file:../data/db.sqlite nuxt dev",
"db:migrate": "DATABASE_URL=file:../data/db.sqlite drizzle-kit migrate",
"db:studio": "DATABASE_URL=file:../data/db.sqlite drizzle-kit studio"
}Why ../data/ instead of ./data/?
The database is stored OUTSIDE the app/ directory to avoid Vite/Nuxt file watcher conflicts. See DATABASE_LOCATION_MIGRATION.md for details.
/your-workspace/
├── data/
│ └── db.sqlite # Development database (outside app/)
├── app/
│ ├── .env # API keys, feature flags (NOT database path!)
│ ├── package.json # Scripts set DATABASE_URL
│ └── ...
.env (in app/ directory):
# NOTE: Do NOT set DATABASE_URL here for development!
# package.json scripts handle database path.
# Voice features
VOICE_ENABLED=true
VOICE_FREE_LIMIT=50
GROQ_API_KEY=your_key_herecd app
bun install
bun run dev # Starts dev server at localhost:3000
bun run db:migrate # Applies migrations to dev databaseFor deploying Tada on your own server using Docker or CapRover.
The Dockerfile sets the database path:
ENV DATABASE_URL=file:/data/db.sqliteYou must mount a persistent volume to /data/ for data to survive container restarts.
services:
tada:
build: .
ports:
- "3000:3000"
volumes:
- tada-data:/data
environment:
- APP_URL=https://tada.example.com
# Optionally enable voice features:
# - GROQ_API_KEY=your_key
volumes:
tada-data:- Create app with persistent storage enabled
- Set persistent directory: Path in App =
/data - Deploy via
caprover deployor GitHub integration - Migrations run automatically on container start
There are two kinds of env vars in Docker/CapRover deployments:
- Server-only — plain names (e.g.
GROQ_API_KEY). Server code readsprocess.envdirectly at request time. - Browser-visible — must use
NUXT_PUBLIC_prefix (e.g.NUXT_PUBLIC_IS_CLOUD_MODE). These are the only way to pass config to the browser after the Docker image is built.
| Variable | Required | Default | Purpose |
|---|---|---|---|
DATABASE_URL |
No | file:/data/db.sqlite |
Database path |
NODE_ENV |
No | production |
Environment mode |
APP_URL |
Recommended | http://localhost:3000 |
Public URL for emails/links |
VOICE_ENABLED |
No | true |
Enable voice input |
GROQ_API_KEY |
No | - | Voice transcription (Groq Whisper) |
OPENAI_API_KEY |
No | - | Voice transcription fallback |
| Variable | Required | Default | Purpose |
|---|---|---|---|
NUXT_PUBLIC_IS_CLOUD_MODE |
No | false |
Enables subscription UI, cookie consent |
NUXT_PUBLIC_UMAMI_HOST |
No | - | Umami analytics script URL |
NUXT_PUBLIC_UMAMI_WEBSITE_ID |
No | - | Umami website tracking ID |
Why the prefix? Nuxt bakes
runtimeConfig.publicvalues into the build. In Docker, env vars aren't available at build time. TheNUXT_PUBLIC_prefix is Nuxt's mechanism to override those baked-in defaults at container startup. Server-only code doesn't have this limitation — it readsprocess.envat request time.
To enable Umami analytics, set both in your deployment config:
NUXT_PUBLIC_UMAMI_HOST=https://your-umami-instance.com/script.js
NUXT_PUBLIC_UMAMI_WEBSITE_ID=your-website-uuidFor local development with .env, use plain names instead:
UMAMI_HOST=https://your-umami-instance.com/script.js
UMAMI_WEBSITE_ID=your-website-uuidThis is optional and privacy-friendly — no data is collected unless you configure it.
For running Tada as a multi-tenant cloud service with billing and usage limits.
Cloud mode activates when either:
TADA_CLOUD_MODE=true, ORSTRIPE_SECRET_KEYis configured
When cloud mode is enabled:
- Cookie consent banner shown
- Email verification required (with grace period)
- Usage limits enforced (free tier: 1-year data retention)
- Stripe billing integration available
- Subscription UI in account settings
| Variable | Purpose |
|---|---|
TADA_CLOUD_MODE |
Enable cloud features (true/false) |
STRIPE_SECRET_KEY |
Stripe API key for billing |
STRIPE_WEBHOOK_SECRET |
Stripe webhook signature verification |
STRIPE_PRICE_SEEDLING |
Stripe price ID for £1/year tier |
STRIPE_PRICE_SAPLING |
Stripe price ID for £5/year tier |
STRIPE_PRICE_OAK |
Stripe price ID for £12/year tier |
STRIPE_PRICE_REDWOOD |
Stripe price ID for £25/year tier |
STRIPE_PRICE_FOREST |
Stripe price ID for £50/year tier |
TADA_REQUIRE_EMAIL_VERIFICATION |
Require verified email (true/false) |
TADA_VERIFICATION_GRACE_DAYS |
Days before verification required (default: 7) |
TADA_FREE_RETENTION_DAYS |
Data retention for free tier (default: 365) |
SMTP_HOST, SMTP_PORT, etc. |
Email configuration for verification |
| Context | DATABASE_URL | Notes |
|---|---|---|
bun run dev |
file:../data/db.sqlite |
Set by package.json |
bun run db:migrate |
file:../data/db.sqlite |
Set by package.json |
| Docker container | file:/data/db.sqlite |
Set in Dockerfile |
| CapRover | file:/data/db.sqlite |
Different host volume per app |
Key principle: Development uses ../data/ (outside app/ to avoid watchers). Production uses /data/ (container volume mount).
Migrations need to be applied:
cd app
bun run db:migrateCheck the startup log for:
Database path: file:../data/db.sqlite (dev: true)
If it shows ./data/db.sqlite (without ..), you have an outdated config.
Always use the package.json script:
# ✅ Correct
bun run db:migrate
# ❌ Wrong - won't use correct DATABASE_URL
npx drizzle-kit migrate- DATABASE_LOCATION_MIGRATION.md - Why database is outside app/
- DEPLOYMENT.md - Full deployment guide
- DEPLOY_CAPROVER.md - CapRover-specific instructions
Last updated: March 2026