Configuration
TideMeter is configured through environment variables. This page documents all available options.
Database Migrations
Migrations run automatically when a new image starts — there is no separate migrate command. This applies to Docker, docker compose, and Kubernetes alike, so upgrading is just docker pull (or rolling a new image tag) and restarting the container.
What happens on boot:
- PayloadCMS schema is applied:
- Development (
NODE_ENV != "production"): Drizzle “push” mode diffs the collections against the database and applies DDL automatically. Fast iteration, no migration files required. - Production (
NODE_ENV == "production"): versioned migrations underapps/web/src/migrations/are applied viapayload.db.migrate()and tracked in thepayload_migrationstable. Already-applied migrations are skipped on subsequent boots.
- Development (
- The analytics package applies any pending SQL migrations from
packages/analytics/drizzle/againstANALYTICS_DATABASE_URL(or ClickHouse migrations frompackages/analytics/clickhouse/whenANALYTICS_DB_TYPE=clickhouse). A Postgres advisory lock serializes concurrent runners, so rolling deploys with multiple replicas are safe. - If
DEMO_MODE=true, demo data is seeded (idempotent — see Demo Mode below).
If any step fails, the pod fails its readiness probe and Kubernetes halts the rollout — the previous image keeps serving traffic. There is no silent partial-upgrade state.
Init is triggered by any route that imports the Payload config — including /api/health, which is hit by the readiness/startup probes. The database user only needs CREATE privileges; no manual SQL is required.
Overrides
| Variable | Effect |
|---|---|
PAYLOAD_DB_PUSH | Force push mode (true) or migration mode (false), regardless of NODE_ENV. Useful when upgrading an old deployment whose schema was originally created with push and has no payload_migrations rows: set true for a single boot to let push reconcile, then unset it. |
Adding a schema change (for contributors)
In dev (NODE_ENV=development), changes to Payload collections are picked up instantly via push. To ship the change in a production image:
cd apps/web && pnpm exec payload migrate:createCommit the generated file under apps/web/src/migrations/ together with the updated index.ts. For analytics tables, add a numbered .sql file under packages/analytics/drizzle/ and update the Drizzle schema in packages/analytics/src/schema/tables.ts.
Environment Variables
Required
| Variable | Description | Example |
|---|---|---|
DATABASE_URL | PostgreSQL connection string for PayloadCMS | postgresql://user:pass@localhost:5480/tidemeter |
PAYLOAD_SECRET | Secret for PayloadCMS auth (min 32 chars). Required in production — app will refuse to start without it. | Any strong random string |
SESSION_SALT_SECRET | Secret for hashing visitor IDs (rotated daily). Required in production. | Any strong random string |
Application
| Variable | Default | Description |
|---|---|---|
NEXT_PUBLIC_APP_URL | http://localhost:3700 | Public URL of your TideMeter instance |
NODE_ENV | development | development or production |
Analytics Database
| Variable | Default | Description |
|---|---|---|
ANALYTICS_DB_TYPE | postgresql | Analytics storage backend: postgresql, clickhouse, or sqlite |
ANALYTICS_DATABASE_URL | Same as DATABASE_URL | PostgreSQL connection string for analytics |
CLICKHOUSE_URL | http://localhost:8123 | ClickHouse HTTP endpoint |
CLICKHOUSE_DATABASE | tidemeter_analytics | ClickHouse database name |
CLICKHOUSE_USER | default | ClickHouse username |
CLICKHOUSE_PASSWORD | — | ClickHouse password (optional) |
ANALYTICS_SQLITE_PATH | ./data/analytics.db | SQLite file path (when using SQLite) |
Optional
| Variable | Default | Description |
|---|---|---|
GEOIP_DB_PATH | — | Path to MaxMind GeoLite2-City.mmdb for geolocation |
DEMO_MODE | false | When true, seeds a demo user, website, ~1500 events, and example funnels on boot |
Email (optional)
TideMeter uses email only for password reset and account verification. If you don’t need those flows (single-user or private deployment), skip this entirely — Payload will print a one-time WARN: No email adapter provided on startup and write any outgoing email to stdout. Login, first-user creation (/admin/create-first-user) and password change from /admin/account all work without email.
Two backends are supported. They are picked in this order, so setting both is unnecessary:
- Resend (
RESEND_API_KEYset) — Resend’s HTTP API via@payloadcms/email-resend. No SMTP port required, lightweight, recommended if you don’t already run a mail server. - SMTP / Nodemailer (
SMTP_HOSTset) — works with any SMTP provider: your own Postfix, Gmail, SendGrid, Mailgun, Postmark, AWS SES, etc.
Option 1 — Resend
| Variable | Default | Description |
|---|---|---|
RESEND_API_KEY | — | Resend API key (re_…). Setting this enables Resend. |
SMTP_FROM_ADDRESS | no-reply@localhost | From address (must be a verified Resend sender / domain) |
SMTP_FROM_NAME | TideMeter | From name |
Option 2 — SMTP (Nodemailer)
| Variable | Default | Description |
|---|---|---|
SMTP_HOST | — | SMTP server hostname. Setting this enables SMTP delivery. |
SMTP_PORT | 587 | SMTP server port |
SMTP_USER | — | SMTP username (see provider notes below) |
SMTP_PASSWORD | — | SMTP password / API key |
SMTP_SECURE | auto (true on 465) | true for implicit TLS (port 465). Otherwise STARTTLS is auto-negotiated. |
SMTP_FROM_ADDRESS | no-reply@localhost | From address for outgoing email |
SMTP_FROM_NAME | TideMeter | From name for outgoing email |
Provider-specific SMTP_USER conventions:
| Provider | SMTP_USER | SMTP_PASSWORD |
|---|---|---|
| SendGrid | literal string apikey | your SendGrid API key |
| Mailgun | postmaster@<your-domain> | the Mailgun SMTP password |
| AWS SES | SES SMTP username (generated in the SES console) | SES SMTP password (not your AWS key) |
| Gmail | full Gmail address | a Google app password |
| Postmark | your Postmark server token | the same server token |
| Postfix | local SASL user (or omit both for unauth relays) | local SASL password |
Buffer Settings
The event buffer can be tuned via code constants in apps/web/src/lib/ingestion/buffer.ts:
| Setting | Default | Description |
|---|---|---|
FLUSH_SIZE | 100 | Max events before flush |
FLUSH_INTERVAL | 5000 | Max milliseconds between flushes |
MAX_BUFFER_SIZE | 10000 | Max buffered events (prevents OOM under DB outage) |
Example .env File
# Database (PostgreSQL for PayloadCMS)
DATABASE_URL=postgresql://postgres:postgres@localhost:5480/tidemeter
# PayloadCMS
PAYLOAD_SECRET=a-very-long-and-random-secret-key-32-chars-minimum
# Privacy
SESSION_SALT_SECRET=another-random-secret-for-visitor-hashing
# Application
NEXT_PUBLIC_APP_URL=http://localhost:3700
NODE_ENV=development
# Analytics database (defaults to PostgreSQL)
ANALYTICS_DB_TYPE=postgresql
# ANALYTICS_DATABASE_URL=postgresql://postgres:postgres@localhost:5480/tidemeter
# For ClickHouse analytics
# ANALYTICS_DB_TYPE=clickhouse
# CLICKHOUSE_URL=http://localhost:8124
# CLICKHOUSE_DATABASE=tidemeter_analytics
# CLICKHOUSE_USER=default
# CLICKHOUSE_PASSWORD=
# For SQLite analytics
# ANALYTICS_DB_TYPE=sqlite
# ANALYTICS_SQLITE_PATH=./data/analytics.db
# Optional: GeoIP
# GEOIP_DB_PATH=/path/to/GeoLite2-City.mmdb
# Optional: Demo mode (seeds [email protected] / demodemo + sample data)
# DEMO_MODE=trueDemo Mode
Set DEMO_MODE=true to start TideMeter pre-populated with a demo user and sample analytics data. Useful for evaluation, screenshots, and public sandboxes.
On first startup the container will:
- Create the demo user
[email protected]with passworddemodemo - Create a sample website (
demo.example.com) - Generate ~1500 analytics events spanning the last 90 days
- Create three example funnels
Seeding is idempotent — it only runs when the demo data is missing, so restarts and upgrades are safe. To re-seed, wipe the database volume.
# Compose overlay (PostgreSQL + DEMO_MODE=true)
docker compose -f docker/docker-compose.yml -f docker/docker-compose.demo.yml up -d
# Or with `docker run`
docker run -d -p 3700:3000 \
-e DATABASE_URL="postgresql://..." \
-e PAYLOAD_SECRET="..." \
-e SESSION_SALT_SECRET="..." \
-e DEMO_MODE=true \
tidemeter/tidemeter:latestThe public instance at demo.tidemeter.com runs with this flag enabled.
Docker Configuration
PostgreSQL Only
docker compose -f docker/docker-compose.yml up -dDefault ports:
- Application: 3700
- PostgreSQL: 5480
PostgreSQL + ClickHouse
docker compose -f docker/docker-compose.yml -f docker/docker-compose.ch.yml up -dDefault ports:
- Application: 3700
- PostgreSQL: 5480
- ClickHouse HTTP: 8124
- ClickHouse Native: 9001
PayloadCMS Configuration
The PayloadCMS configuration is in apps/web/src/payload.config.ts. Key settings:
- Collections: Users, Teams, TeamMembers, Websites, ApiKeys, Funnels
- Auth: Cookie-based authentication with
payload-tokenHTTP-only cookie - Admin: Available at
/admin - REST API: Available at
/api/{collection-slug} - Editor: Lexical rich-text editor
Tailwind CSS 4
TideMeter uses Tailwind CSS 4 with the CSS-first configuration approach:
- Theme is defined in
apps/web/src/app/globals.cssusing@theme - No
tailwind.config.jsfile - PostCSS plugin:
@tailwindcss/postcss
TypeScript
TypeScript strict mode is enabled across all packages via @tidemeter/tsconfig. Key conventions:
- Target: ES2022, module: ESNext, moduleResolution: bundler
- No
anytypes (unless absolutely necessary) - Named exports for all modules
- No
.jsextensions in imports (Turbopack doesn’t resolve them) noUncheckedIndexedAccessenabled