Developer Guide
Technical architecture, feature-flag model, and provider abstractions used by Owlat.
This guide covers the technical architecture and development patterns used in Owlat. Owlat is built as a set of feature areas (sending, receiving, AI, integrations, security, deliverability) that share a common platform — the Convex backend, the email rendering engine, and the feature-flag registry that turns each area on or off.
Quick Links
Architecture & core systems
- Architecture Overview — system design, monorepo layout, data flow
- Feature flags — registry, dependency resolution, docker profile mapping
- Providers — pluggable LLM and email/send providers
- Convex Backend — database schema and API functions
- Authentication — BetterAuth integration
- Environment Variables — complete reference
Outbound mail
- Email System — editor, templates, and sending
- Email Renderer — block-to-HTML rendering engine
- Email Security — content scanning, file validation, URL reputation, ClamAV
- MTA System — outbound Mail Transfer Agent
- How Email Works — SMTP, SPF, DKIM, DMARC primer
Inbound mail
- Postbox Architecture — personal mailbox schema, IMAP server, app-password auth, outbound relay
Self-hosting
- Self-hosting — install on a VPS in 10 minutes
- Self-hosting configuration — env vars and overrides
- DNS & email — DNS records, rDNS, DKIM
- Production — hardening and tuning
- Maintenance — backups, upgrades, observability
Reference
- Component Library — reusable UI components
- Scopes — permission model
- Architectural Decisions — ADRs
- Contributing — how to contribute to Owlat
Development Setup
Prerequisites
- Node.js 22+
- Bun package manager
- Docker + Docker Compose v2 (for the full self-host stack)
Local Development
- Install dependencies:
bun install - Start the Convex backend (keeps running):
bun run dev:api - Start the Nuxt frontend:
bun run dev
Environment Variables
Both the Nuxt frontend and the Convex backend use environment variables for configuration. Feature flags follow a dual-variable pattern: one Convex env var for the backend and one NUXT_PUBLIC_* var for the frontend.
See the Environment Variables page for a complete reference with setup instructions for providers, security, and analytics.
Quick reference — essential variables:
| Variable | Where | Purpose |
|---|---|---|
BETTER_AUTH_SECRET | Convex | Session signing secret |
SITE_URL | Convex | Public site URL for redirects |
EMAIL_PROVIDER | Convex | mta (default), ses, or resend |
LLM_PROVIDER | Convex | openai (default), openrouter, ollama (all OpenAI-compatible) — only required when ai flag is on. A Claude endpoint is reachable only by pointing LLM_BASE_URL at an OpenAI-compatible proxy, not by setting LLM_PROVIDER=anthropic |
UNSUBSCRIBE_SECRET | Convex | HMAC secret for unsubscribe tokens |
NUXT_PUBLIC_CONVEX_URL | Nuxt .env | Convex deployment URL |
NUXT_PUBLIC_CONVEX_SITE_URL | Nuxt .env | Convex site URL (for auth) |
NUXT_PUBLIC_SITE_URL | Nuxt .env | Public site URL |
Code Quality
bun run typecheck # TypeScript checking
bun run lint # Oxlint
bun run ox:fmt # Format with Oxfmt
Project Structure
owlat/
├── apps/
│ ├── web/ # Nuxt 4 dashboard
│ │ └── app/
│ │ ├── components/ # Vue components
│ │ │ └── ui/ # App-specific UI components
│ │ ├── composables/ # Vue composables
│ │ ├── layouts/ # Page layouts
│ │ ├── pages/ # File-based routing
│ │ └── plugins/ # Nuxt plugins
│ ├── api/ # Convex backend
│ │ └── convex/
│ │ ├── _generated/ # Auto-generated (don't edit)
│ │ ├── lib/ # Shared utilities + provider factories
│ │ │ ├── llm/ # LLM helpers
│ │ │ ├── llmProvider.ts # OpenAI-compatible LLM dispatch
│ │ │ ├── sendProviders/ # MTA, Resend, SES senders + routing/dispatch
│ │ │ ├── emailProviders/ # Identity + domain verification (mtaIdentity, sesIdentity, domainVerification)
│ │ │ └── posthog.ts # PostHog analytics
│ │ ├── schema.ts # Database schema
│ │ ├── mail/ # Postbox / mailbox modules
│ │ └── *.ts # API functions
│ ├── mta/ # Outbound Mail Transfer Agent (SMTP sender + scan endpoint)
│ ├── imap/ # IMAP4rev1 server (Postbox personal mail)
│ ├── mail-sync/ # Syncs users' external IMAP/SMTP accounts (IMAP IDLE inbound + outbound relay)
│ ├── setup-cli/ # `owlat-setup` — wizard, feature/pack/env management
│ ├── updater/ # In-place update sidecar
│ ├── docs/ # Nuxt Content documentation
│ ├── marketing/ # Marketing / landing page (Nuxt)
│ ├── desktop/ # Desktop client shell (Tauri)
│ └── code-worker/ # Code-task worker (for `codeWorkTasks`)
├── infra/
│ └── templates/ # Per-instance VPS compose, ACME (lego) sidecar, env template
├── docker-compose.yml # Root self-host stack (web/convex/mta + optional Caddy --profile tls)
├── Caddyfile.example # Reference reverse-proxy config for --profile tls
└── packages/
├── email-builder/ # Vue email editor component
├── email-renderer/ # JSON blocks → HTML renderer
├── email-scanner/ # Email security scanning
├── email-previewer/ # Email preview / compatibility analysis
├── channels/ # Notification channel abstractions
├── shared/ # Shared types, validation, feature flag registry
├── ui/ # Nuxt layer: shared UI components and composables
├── sdk-js/ # Official TypeScript SDK
└── sdk-java/ # Official Java SDK
Key Technologies
| Technology | Purpose |
|---|---|
| Nuxt 4 | Vue 3 full-stack framework |
| Convex | Real-time serverless backend |
| BetterAuth | Authentication with organization support |
| Tailwind CSS 4 | Utility-first styling |
| @owlat/email-renderer | Custom HTML email rendering |
| Hono | HTTP framework (MTA scan/relay endpoints) |
| GroupMQ + Redis | MTA job queue |
| Caddy | Bundled HTTPS reverse proxy for web/Convex/MTA (--profile tls) |
| lego / ACME | Self-host TLS via DNS-01 for IMAP/SMTP mail certs |
| Docker Compose profiles | Optional services per feature flag |
| Lucide | Icon library |
Patterns and Conventions
File Naming
- Vue components:
PascalCase.vue - Composables:
useCamelCase.ts - Convex functions:
camelCase.ts - Pages:
kebab-case.vueor[param].vue
Imports
Use the ~/ alias for imports within the Nuxt app:
import { useAuth } from '~/composables/useAuth';
The feature-flag registry is imported from the shared package on every layer:
import { FEATURE_FLAGS, resolveFlags, applyToggle } from '@owlat/shared/featureFlags';
TypeScript
All code is TypeScript. Use explicit types for function parameters and return values:
function formatDate(timestamp: number): string {
return new Date(timestamp).toLocaleDateString();
}
Error Handling
- Frontend: Use
useToast()for user feedback - Backend: Throw
Errorwith descriptive messages - API: Return structured error responses
// Frontend
const { showToast } = useToast();
try {
await mutation();
showToast('Saved successfully');
} catch (e) {
showToast('Failed to save', 'error');
}
// Backend (Convex)
if (!organizationId) {
throw new Error('Organization not found');
}
Feature-gated routes
Pages that belong to a toggleable feature declare it via definePageMeta:
definePageMeta({
layout: 'dashboard',
middleware: 'auth',
requiresFeature: 'postbox',
});
A global middleware redirects to a "feature off" page if the flag is disabled. Convex queries also check the flag server-side so a stale client can't bypass it.