Environment Variables
Owlat uses environment variables to configure payments, email sending, authentication, and feature flags. Backend variables are set via the Convex...
Owlat uses environment variables to configure payments, email sending, authentication, and feature flags. Backend variables are set via the Convex dashboard; frontend variables live in .env files.
Most readers of this page are self-hosting Owlat. Skip down to Self-hosted configuration — it's the canonical reference for what to put in your .env.
The sections below that (Infrastructure Billing, Hetzner provisioning, tier management) apply only to operators running the hosted cloud — the apps/nest-api control plane. Self-hosters can ignore them; they live behind --profile hosted in docker-compose.yml and are disabled by default.
Self-hosted configuration
Self-hosters put everything in a single .env file at the repo root. Start from the template:
cp .env.selfhost.example .env
The template and docker-compose.yml reference these variables:
Required secrets
| Variable | Description | Generate with |
|---|---|---|
INSTANCE_SECRET | Convex backend signing secret. Also used by the updater sidecar for authenticated calls. | openssl rand -hex 32 |
CONVEX_ADMIN_KEY | Admin key for deploying functions. Generate after first boot via docker compose exec convex ./generate_admin_key.sh. | (see above) |
MTA_API_KEY | API key the tenant app uses to enqueue sends in the MTA. | openssl rand -base64 32 |
MTA_WEBHOOK_SECRET | HMAC secret for MTA → Convex delivery-event callbacks. | openssl rand -base64 32 |
Public URLs
| Variable | Description | Default |
|---|---|---|
NUXT_PUBLIC_CONVEX_URL | Browser-facing Convex URL. Must match what you expose via reverse proxy. | http://localhost:3210 |
NUXT_PUBLIC_CONVEX_SITE_URL | Browser-facing Convex HTTP-actions URL. | http://localhost:3211 |
NUXT_PUBLIC_SITE_URL | Public URL of the web app. | http://localhost:3000 |
MTA (mail sending)
| Variable | Description | Default |
|---|---|---|
EHLO_HOSTNAME | Must match your server's rDNS PTR record. | mail.localhost |
RETURN_PATH_DOMAIN | Domain used for VERP bounce return-path addresses. | bounces.localhost |
IP_POOLS_TRANSACTIONAL | Comma-separated IP(s) for transactional sends. | 127.0.0.1 |
IP_POOLS_CAMPAIGN | Comma-separated IP(s) for marketing campaigns. | 127.0.0.1 |
DKIM_KEYS | JSON: {"example.com":{"selector":"s1","privateKey":"..."}}. | {} |
WORKER_CONCURRENCY | Max concurrent SMTP deliveries. | 50 |
MTA_LOG_LEVEL | Log verbosity (debug/info/warn/error). | info |
Deployment mode
| Variable | Description | Default |
|---|---|---|
OWLAT_DEPLOYMENT_MODE | selfhost or hosted. Controls first-run UX, billing visibility, and upgrade prompts in the web app. | selfhost |
OWLAT_HOSTED_MODE | true to enable hosted-cloud control-plane features (Stripe, provisioning, tiers). Self-hosters leave this false. | false |
OWLAT_VERSION | Build-time version injected into images for in-app update checks. Set automatically by CI. | dev |
Port overrides (optional)
All ports default to sensible values. Override only if the defaults conflict:
# CONVEX_PORT=3210
# CONVEX_SITE_PORT=3211
# DASHBOARD_PORT=6791
# WEB_PORT=3000
# MTA_HTTP_PORT=3100
# MTA_SMTP_PORT=25
# REDIS_PORT=6379
# CLAMAV_PORT=3310
Optional: PostHog analytics
# NUXT_PUBLIC_POSTHOG_API_KEY=
# NUXT_PUBLIC_POSTHOG_HOST=https://eu.i.posthog.com
Everything below this point — Stripe billing, Hetzner provisioning, tier management, the control-plane environment — applies only if you're running the managed cloud offering (not self-hosting). Self-hosters can stop reading here.
Where to Set Variables
Convex backend — tenant app (all process.env references in apps/api/):
npx convex env set VAR_NAME value
Or set them in the Convex dashboard under your deployment's Settings > Environment Variables.
Convex backend — control plane (all process.env references in apps/nest-api/):
The control plane runs its own Convex deployment. Set variables via the Convex dashboard for the nest-api deployment.
Nuxt frontend (apps/web/.env):
NUXT_PUBLIC_CONVEX_URL=https://your-deployment.convex.cloud
NUXT_PUBLIC_CONVEX_SITE_URL=https://your-deployment.convex.site
NUXT_PUBLIC_SITE_URL=http://localhost:3000
Infrastructure Billing (Control Plane)
Billing is managed by the control plane (apps/nest-api), not the tenant app. Each organization subscribes to an infrastructure tier that determines their VPS specifications and monthly price.
Pricing Tiers
| Tier | Server Type | Specs | Monthly Price |
|---|---|---|---|
| Starter | cx22 | 2 vCPU, 4 GB RAM, 40 GB disk | €28 |
| Growth | cx32 | 4 vCPU, 8 GB RAM, 80 GB disk | €60 |
| Enterprise | cx42 | 8 vCPU, 16 GB RAM, 160 GB disk | €120 |
Prerequisites
- A Stripe account
- Three products with monthly recurring prices (one per tier)
Required Variables
Set these in the nest-api Convex dashboard:
| Variable | Description |
|---|---|
STRIPE_SECRET_KEY | Your Stripe secret API key (starts with sk_). |
STRIPE_WEBHOOK_SECRET | Stripe webhook signing secret (starts with whsec_). |
STRIPE_PRICE_STARTER | Stripe Price ID for the Starter tier (€28/mo). |
STRIPE_PRICE_GROWTH | Stripe Price ID for the Growth tier (€60/mo). |
STRIPE_PRICE_ENTERPRISE | Stripe Price ID for the Enterprise tier (€120/mo). |
Setup Steps
- Create a Stripe account at stripe.com and note your secret key from the API Keys page.
- Create three products in Stripe — one for each tier (Starter, Growth, Enterprise) with monthly recurring pricing. Copy the Price IDs (start with
price_). - Set the environment variables in the nest-api Convex dashboard:
npx convex env set STRIPE_SECRET_KEY sk_live_... npx convex env set STRIPE_WEBHOOK_SECRET whsec_... npx convex env set STRIPE_PRICE_STARTER price_... npx convex env set STRIPE_PRICE_GROWTH price_... npx convex env set STRIPE_PRICE_ENTERPRISE price_... - Configure a Stripe webhook pointing to the nest-api Convex site URL. Subscribe to events:
invoice.paid,invoice.payment_failed,customer.subscription.updated,customer.subscription.deleted.
Infrastructure (Hetzner)
The control plane provisions VPS instances on Hetzner Cloud. These variables are set in the nest-api Convex dashboard.
Required Variables
| Variable | Description |
|---|---|
HETZNER_API_TOKEN | Hetzner Cloud API token for creating, resizing, and deleting servers. |
HETZNER_SSH_KEY_IDS | Comma-separated Hetzner SSH key IDs to inject into provisioned servers (for emergency access). |
Setup Steps
- Create a Hetzner Cloud project at console.hetzner.cloud.
- Generate an API token with read/write permissions from the project's Security > API Tokens page.
- (Optional) Upload an SSH key in the project's Security > SSH Keys page. Copy the key ID.
- Set the environment variables:
npx convex env set HETZNER_API_TOKEN your-api-token npx convex env set HETZNER_SSH_KEY_IDS 12345,67890
Email Sending
Provider Selection
Owlat supports three email providers: a custom MTA for direct SMTP delivery (default), AWS SES, and Resend. Set the EMAIL_PROVIDER variable in the Convex dashboard:
npx convex env set EMAIL_PROVIDER mta # default
npx convex env set EMAIL_PROVIDER resend
npx convex env set EMAIL_PROVIDER mta
AWS SES
| Variable | Description |
|---|---|
AWS_SES_REGION | AWS region for SES (e.g., eu-west-1, us-east-1). Required. |
AWS_SES_ACCESS_KEY_ID | IAM access key ID with SES permissions. Required. |
AWS_SES_SECRET_ACCESS_KEY | IAM secret access key. Required. |
Setup Steps
- Create an IAM user in AWS with programmatic access and the
AmazonSESFullAccesspolicy (or a scoped policy allowingses:SendEmailandses:SendRawEmail). - Verify your sending domain in the SES console. SES requires domain verification before you can send emails.
- Request production access if your SES account is in sandbox mode. Sandbox mode limits sending to verified addresses only.
- Set the environment variables:
npx convex env set EMAIL_PROVIDER ses npx convex env set AWS_SES_REGION eu-west-1 npx convex env set AWS_SES_ACCESS_KEY_ID AKIA... npx convex env set AWS_SES_SECRET_ACCESS_KEY ...
Resend
| Variable | Description |
|---|---|
RESEND_API_KEY | Resend API key. Required when using Resend. Also used for organization invitation emails. |
RESEND_WEBHOOK_SECRET | Webhook signing secret (format: whsec_<base64>). Required for delivery event tracking. |
Setup Steps
- Create a Resend account at resend.com and add your sending domain.
- Generate an API key from the Resend dashboard.
- (Optional) Set up webhooks for delivery tracking:
- In Resend, create a webhook pointing to your Convex site URL's webhook endpoint
- Copy the webhook signing secret
- Set the environment variables:
npx convex env set EMAIL_PROVIDER resend npx convex env set RESEND_API_KEY re_... npx convex env set RESEND_WEBHOOK_SECRET whsec_...
Custom MTA
When using the custom MTA (EMAIL_PROVIDER=mta), the Convex backend connects to the MTA service over HTTP. See the MTA System docs for full architecture details.
| Variable | Description |
|---|---|
MTA_API_URL | MTA service URL (e.g., http://mta.internal:3100). Required when using MTA. |
MTA_API_KEY | Shared API key for MTA authentication (Bearer token). Required when using MTA. |
MTA_WEBHOOK_SECRET | Shared secret for authenticating MTA webhook callbacks. Required when using MTA. |
Setup Steps
- Deploy the MTA service from
apps/mta/(see the MTA System docs for configuration). - Set the environment variables in the Convex dashboard:
npx convex env set EMAIL_PROVIDER mta npx convex env set MTA_API_URL http://mta.internal:3100 npx convex env set MTA_API_KEY your-shared-api-key npx convex env set MTA_WEBHOOK_SECRET your-webhook-secret
Common Email Configuration
These variables apply regardless of which provider you use:
| Variable | Description | Default |
|---|---|---|
DEFAULT_FROM_EMAIL | Default sender email address for transactional emails | noreply@example.com |
DEFAULT_FROM_NAME | Default sender display name | Owlat |
DEFAULT_FROM_DOMAIN | Domain used for system emails (e.g., invitation emails). Prepended with noreply@. | mail.owlat.app |
CONVEX_SITE_URL | Your Convex site URL. Used for tracking pixels and unsubscribe links in campaign emails. | — |
UNSUBSCRIBE_SECRET | Secret key for generating HMAC-signed unsubscribe tokens. Required. | — |
Email Security
Optional environment variables for email content and attachment scanning. See the Email Security docs for details on the scanning pipeline.
Variables
Set in the Convex dashboard:
| Variable | Description |
|---|---|
GOOGLE_SAFE_BROWSING_API_KEY | Google Safe Browsing API v4 key for URL reputation checking. Free tier: 10,000 requests/day. |
MTA_INTERNAL_URL | MTA internal URL for attachment scanning (e.g., http://mta.internal:3100). Required for ClamAV integration. |
Set in the MTA environment:
| Variable | Default | Description |
|---|---|---|
CLAMAV_HOST | localhost | ClamAV daemon hostname |
CLAMAV_PORT | 3310 | ClamAV daemon port |
Setup Steps
- (Optional) Enable URL reputation checking — get a Google Safe Browsing API key from the Google Cloud Console:
npx convex env set GOOGLE_SAFE_BROWSING_API_KEY AIza... - (Optional) Enable ClamAV malware scanning — deploy ClamAV alongside the MTA (see MTA System > ClamAV Sidecar) and set the internal URL:
npx convex env set MTA_INTERNAL_URL http://mta.internal:3100
Content scanning (spam keywords, phishing URLs, homoglyphs) and file type validation (magic bytes, double extensions) work out of the box with no additional configuration. Only URL reputation and ClamAV require env vars.
Analytics (PostHog)
Owlat integrates with PostHog for product analytics and error tracking. The integration is optional — everything works without it.
Variables
Convex dashboard:
| Variable | Description |
|---|---|
POSTHOG_API_KEY | PostHog project API key (starts with phc_). Required to enable server-side tracking. |
POSTHOG_HOST | PostHog instance URL. Defaults to https://eu.i.posthog.com (EU cloud). |
Nuxt frontend (apps/web/.env):
| Variable | Description |
|---|---|
NUXT_PUBLIC_POSTHOG_API_KEY | PostHog project API key. Required to enable client-side tracking. |
NUXT_PUBLIC_POSTHOG_HOST | PostHog instance URL. Defaults to https://eu.i.posthog.com. |
Setup Steps
- Create a PostHog project at posthog.com and copy the project API key.
- Add the
organizationgroup type in PostHog: go to Settings → Groups → add a group type calledorganization. This enables organization-level analytics. - Set the environment variables:
# Convex (server-side events) npx convex env set POSTHOG_API_KEY phc_... npx convex env set POSTHOG_HOST https://eu.i.posthog.com # apps/web/.env (client-side events) NUXT_PUBLIC_POSTHOG_API_KEY=phc_... NUXT_PUBLIC_POSTHOG_HOST=https://eu.i.posthog.com
Tracked Events
Client-side (automatic):
$pageview— SPA page navigations$pageleave— page leave events$exception— Vue errors and unhandled promise rejections- User identification and organization group association
Server-side (fire-and-forget from mutations):
campaign_created,campaign_sentcontact_createdautomation_created,automation_activated,automation_pausedtopic_created
All server events include organizationId as a property and PostHog group.
Translations (AI)
Owlat supports AI-powered email template translations. OpenRouter is the primary provider; OpenAI is used as a fallback if OpenRouter is not configured.
| Variable | Description |
|---|---|
OPENROUTER_API_KEY | OpenRouter API key for AI translations (primary). |
OPENAI_API_KEY | OpenAI API key for AI translations (fallback if OpenRouter is not set). |
Set in the Convex dashboard:
npx convex env set OPENROUTER_API_KEY sk-or-...
# or, as fallback:
npx convex env set OPENAI_API_KEY sk-...
Authentication
| Variable | Description | Default |
|---|---|---|
BETTER_AUTH_SECRET | Secret key for BetterAuth session signing. Required. | — |
SITE_URL | Public site URL for auth redirects and callbacks. | http://localhost:3000 |
Set BETTER_AUTH_SECRET in the Convex dashboard. Use a long, random string:
npx convex env set BETTER_AUTH_SECRET $(openssl rand -base64 32)
Nuxt Frontend
These go in apps/web/.env:
| Variable | Description |
|---|---|
NUXT_PUBLIC_CONVEX_URL | Your Convex deployment URL (e.g., https://your-deployment.convex.cloud) |
NUXT_PUBLIC_CONVEX_SITE_URL | Your Convex site URL (e.g., https://your-deployment.convex.site). Used for auth proxy. |
NUXT_PUBLIC_SITE_URL | Public site URL. Must match SITE_URL set in Convex. |
Admin Dashboard
The nest dashboard (apps/nest/) uses its own .env file. It connects to a separate Convex deployment (apps/nest-api), not the same backend as the tenant web app.
Set in apps/nest/.env:
| Variable | Description |
|---|---|
NUXT_PUBLIC_CONVEX_URL | Convex deployment URL for the nest-api deployment |
NUXT_PUBLIC_CONVEX_SITE_URL | Convex site URL for the nest-api deployment (used for auth proxy) |
NUXT_PUBLIC_SITE_URL | Admin dashboard URL. Defaults to http://localhost:3001. |
Complete Reference
Tenant App (apps/api)
| Variable | Where | Required | Default | Purpose |
|---|---|---|---|---|
SITE_URL | Convex | Yes | http://localhost:3000 | Public site URL for redirects |
EMAIL_PROVIDER | Convex | No | mta | Email provider (mta, ses, or resend) |
AWS_SES_REGION | Convex | If SES | — | AWS region for SES |
AWS_SES_ACCESS_KEY_ID | Convex | If SES | — | AWS IAM access key |
AWS_SES_SECRET_ACCESS_KEY | Convex | If SES | — | AWS IAM secret key |
RESEND_API_KEY | Convex | If Resend | — | Resend API key |
RESEND_WEBHOOK_SECRET | Convex | No | — | Resend webhook signing secret |
MTA_API_URL | Convex | If MTA | — | MTA service URL |
MTA_API_KEY | Convex | If MTA | — | MTA API authentication key |
MTA_WEBHOOK_SECRET | Convex | If MTA | — | MTA webhook signing secret |
DEFAULT_FROM_EMAIL | Convex | No | noreply@example.com | Default sender email |
DEFAULT_FROM_NAME | Convex | No | Owlat | Default sender name |
DEFAULT_FROM_DOMAIN | Convex | No | mail.owlat.app | Domain for system emails |
CONVEX_SITE_URL | Convex | Yes | — | Convex site URL for tracking/unsubscribe |
UNSUBSCRIBE_SECRET | Convex | Yes | — | HMAC secret for unsubscribe tokens |
BETTER_AUTH_SECRET | Convex | Yes | — | Session signing secret |
OPENROUTER_API_KEY | Convex | No | — | AI translations (primary provider) |
OPENAI_API_KEY | Convex | No | — | AI translations (fallback provider) |
POSTHOG_API_KEY | Convex | No | — | PostHog project API key (server-side) |
POSTHOG_HOST | Convex | No | https://eu.i.posthog.com | PostHog instance URL (server-side) |
GOOGLE_SAFE_BROWSING_API_KEY | Convex | No | — | URL reputation checking (Safe Browsing API) |
MTA_INTERNAL_URL | Convex | No | — | MTA URL for attachment scanning |
Control Plane (apps/nest-api)
| Variable | Where | Required | Default | Purpose |
|---|---|---|---|---|
BETTER_AUTH_SECRET | nest-api Convex | Yes | — | Session signing for admin auth |
SITE_URL | nest-api Convex | Yes | http://localhost:3001 | Admin dashboard URL for auth redirects |
HETZNER_API_TOKEN | nest-api Convex | Yes | — | Hetzner Cloud API token for VPS provisioning |
HETZNER_SSH_KEY_IDS | nest-api Convex | No | — | Comma-separated SSH key IDs for server access |
STRIPE_SECRET_KEY | nest-api Convex | Yes | — | Stripe API secret key for subscriptions |
STRIPE_WEBHOOK_SECRET | nest-api Convex | Yes | — | Stripe webhook signing secret |
STRIPE_PRICE_STARTER | nest-api Convex | Yes | — | Stripe Price ID for Starter tier |
STRIPE_PRICE_GROWTH | nest-api Convex | Yes | — | Stripe Price ID for Growth tier |
STRIPE_PRICE_ENTERPRISE | nest-api Convex | Yes | — | Stripe Price ID for Enterprise tier |
Frontend (apps/web and apps/nest)
| Variable | Where | Required | Default | Purpose |
|---|---|---|---|---|
NUXT_PUBLIC_CONVEX_URL | Nuxt .env | Yes | — | Convex deployment URL |
NUXT_PUBLIC_CONVEX_SITE_URL | Nuxt .env | Yes | — | Convex site URL for auth |
NUXT_PUBLIC_SITE_URL | Nuxt .env | Yes | — | Public site URL |
NUXT_PUBLIC_POSTHOG_API_KEY | Nuxt .env | No | — | PostHog project API key (client-side) |
NUXT_PUBLIC_POSTHOG_HOST | Nuxt .env | No | https://eu.i.posthog.com | PostHog instance URL (client-side) |
MTA (apps/mta)
| Variable | Where | Required | Default | Purpose |
|---|---|---|---|---|
CLAMAV_HOST | MTA env | No | localhost | ClamAV daemon hostname |
CLAMAV_PORT | MTA env | No | 3310 | ClamAV daemon port |