Architecture Overview

Owlat follows a modern serverless architecture with real-time capabilities.

Owlat follows a modern serverless architecture with real-time capabilities.

System Architecture

FrontendNuxt 4 + Vue 3
PagesRouter
ComposablesState
Component LibraryUI Components
Real-time subscriptionsMutations / Actions
Convex BackendServerless
QueriesReal-time
MutationsACID
ActionsExternal APIs
SchemaDatabase
BetterAuthAuth
HTTP HandlersREST API
Owlat MTADefault — Direct SMTP delivery
Intelligence pipelineIP warmingBounce processing
or
AWS SES
Resend
WebhooksEvents
PostHogAnalytics & Errors
← Client (posthog-js)← Server (posthog-node)

Monorepo Structure

Apps

AppDescription
apps/webMain web application (Nuxt 4 + Vue 3)
apps/apiBackend (Convex — serverless functions, database, auth)
apps/docsDeveloper documentation (VitePress)
apps/marketingMarketing / landing page site (Nuxt)
apps/mtaCustom Mail Transfer Agent (Hono + GroupMQ + direct SMTP)
apps/nestPlatform administration dashboard (Nuxt 3)
apps/nest-apiControl plane backend (Convex — VPS provisioning, billing, health monitoring)

Packages

PackageDescription
packages/sharedShared types (block types, editor types, compatibility data)
packages/email-builderEmail builder Vue components (Notion-like editor)
packages/email-rendererEmail HTML rendering engine (table-based, VML, CSS inlining)
packages/email-scannerEmail security scanning (content analysis, file validation, URL reputation, ClamAV client)
packages/email-previewerEmail client preview components (compatibility analysis, Can I Email data)
packages/uiNuxt layer providing shared UI components and composables
packages/sdk-jsJavaScript SDK for the Owlat API
packages/sdk-javaJava SDK for the Owlat API

Data Flow

Real-time Updates

Convex provides automatic real-time updates:

// Frontend - automatically re-renders when data changes
const contacts = useConvexQuery(api.contacts.listFromSession, {});

// Backend - changes automatically push to subscribed clients
export const create = mutation({
    handler: async (ctx, args) => {
        await ctx.db.insert('contacts', { ...args });
        // Clients subscribed to list queries automatically update
    },
});

Authentication Flow

User LoginCredentials submitted
BetterAuthAuthentication provider
Session CreatedJWT token issued
Organization SetactiveOrganizationId stored in session
Backend queries extract organizationId from session
Data scoped to organization automatically

Email Sending Flow

Template CreatedJSON Blocks → @owlat/email-renderer → HTML
Domain AddedSES Registration → DNS Records Generated → User Configures DNS
Campaign CreatedAudience Selected → Domain Verified (DNS + SES)
Owlat MTA (default)Direct SMTP · Intelligence pipeline · IP warming
Webhooks → Status Updates → Analytics

Multi-tenancy

Owlat has two levels of multi-tenancy:

  1. Platform-level — The control plane (apps/nest-api) manages multiple VPS instances, one per customer organization. Each VPS runs an isolated Docker Compose stack.
  2. Tenant-level — Within each VPS, the Convex backend scopes all data to organizations via BetterAuth (the pattern below).

All data is scoped to organizations via BetterAuth:

// Schema pattern - every table has organizationId
contacts: defineTable({
    organizationId: v.string(),
    // ... other fields
}).index('by_org', ['organizationId']);

// Query pattern - always filter by organization
const contacts = await ctx.db
    .query('contacts')
    .withIndex('by_org', (q) => q.eq('organizationId', organizationId))
    .collect();

Authentication Architecture

BetterAuth
UsersAuth
SessionsJWT
OrganizationsTeams
Convex Adapter
  • Stores user/session data in Convex
  • Provides auth routes via HTTP handlers
  • Links sessions to organizations

Email System Architecture

Domain management

Domain management (registration, DNS verification, SES identity setup) is handled through the dashboard UI. There is no public API for domain management.

Control Plane Architecture

The control plane (apps/nest-api/) manages the lifecycle of customer organizations. Each organization gets a dedicated VPS on Hetzner Cloud running a full Docker Compose stack.

Provisioning Flow

User signs upSelects tier and region
Stripe subscriptionPayment confirmed via Stripe Elements
Organization created (pending)Provisioning job queued
Hetzner APIServer created with cloud-init userdata
Docker Compose stackConvex + Web + MTA + Redis + ClamAV + convex-deploy
Smoke test passesConvex :3210, Web :3000 responding
Organization activeRouting table updated, instance accessible

Per-VPS Stack

Each provisioned VPS runs six Docker services:

ServiceImagePortPurpose
convexghcr.io/get-convex/convex-backend3210 (API), 3211 (site proxy)Self-hosted Convex backend
webghcr.io/owlat/web3000Nuxt web application
mtaghcr.io/owlat/mta3100 (API), 25 (SMTP)Mail Transfer Agent
redisredis:7-alpine6379Queue and cache layer
clamavclamav/clamav:1.33310Antivirus scanning
convex-deployghcr.io/owlat/convex-deploySchema deployment helper (run once)

Edge Routing

A Caddy reverse proxy reads the routingTable in the control plane database to map organization slugs to VPS IP addresses. When an organization is suspended or deleted, its routing entry is deactivated.

Available Regions

VPS instances can be provisioned in six Hetzner Cloud datacenters:

RegionLocationContinent
fsn1Falkenstein, GermanyEU
nbg1Nuremberg, Germany (default)EU
hel1Helsinki, FinlandEU
ashAshburn, VA, USAUS
hilHillsboro, OR, USAUS
sinSingaporeAsia

VPS Health Monitoring

The control plane runs health checks every 5 minutes against all running VPS instances, probing three services:

ServiceEndpointPort
Convex/version3210
Web/3000
MTA/health3100

Status Levels

StatusCondition
healthyAll three services responding
degradedConvex up + at least one other service up
unhealthyOnly one service responding
unreachableNo services responding

Auto-Recovery

  • Alert at 3 consecutive failures (15 minutes) — creates a health alert for admins
  • Auto-reboot at 6 consecutive failures (30 minutes) — triggers Hetzner server reboot via API

Database Schema Overview

The Convex schema (~30 tables) follows these patterns:

Core Tables

TablePurpose
userProfilesUser profile data linked to BetterAuth
organizationSettingsOrganization-level configuration

Contact Management

TablePurpose
contactsEmail contacts
contactPropertiesCustom field definitions
contactPropertyValuesCustom field values per contact
topicsContact groupings
contactTopicsMany-to-many with DOI status
segmentsSaved filter configurations

Email System

TablePurpose
emailTemplatesMarketing/transactional templates
emailBlocksReusable content blocks
campaignsOne-time email sends (includes archiveToken, archiveHtml, hardBounces, softBounces)
emailSendsPer-recipient tracking
transactionalEmailsAPI-triggered templates
transactionalSendsTransactional delivery tracking

Automations

TablePurpose
automationsWorkflow definitions
automationStepsSteps within workflows
automationRunsContact progress through workflows
automationStepRunsIndividual step execution

Settings & Security

TablePurpose
domainsCustom sending domains
apiKeysAPI authentication
webhooksEvent notifications
webhookDeliveryLogsDelivery tracking
blockedEmailsBounce/complaint blocklist
auditLogsAction history
formEndpointsPublic form configuration
formSubmissionsForm submission records
mediaAssetsMedia library files
contactActivitiesContact activity tracking

Control Plane Tables (apps/nest-api)

TablePurpose
tiersInfrastructure pricing tiers (Starter/Growth/Enterprise) mapped to Hetzner server types
organizationsTenant organizations with lifecycle states (pending → active → suspended → deleted)
vpsesVPS instances with Hetzner server IDs, IPs, health status
routingTableCaddy slug-to-IP routing entries
provisioningJobsJob queue for create/resize/delete/redeploy operations
invoicesMonthly billing invoices per organization
healthAlertsVPS health alerts (unhealthy, auto-reboot, recovered)
auditLogsPlatform admin action logging
platformAdminsAdmin user accounts (admin/superadmin roles)

Background Jobs

Tenant Backend (apps/api/convex/crons.ts)

JobIntervalPurpose
Process scheduled campaigns1 minBackup for scheduler-based sends
Process pending delays5 minCatch missed automation delays
Process account deletions24 hoursHandle expired grace periods
Cleanup webhook logs7 daysRemove logs older than 30 days
Refresh segment counts30 minKeep cached counts fresh

Control Plane (apps/nest-api/convex/crons.ts)

JobIntervalPurpose
Health checks5 minCheck all running VPS instances (Convex, Web, MTA)
Process provisioning jobs1 minExecute queued create/resize/delete/redeploy operations
Generate monthly invoices24 hoursCreate invoices for active organizations, mark overdue

Frontend Architecture

Layouts

  • default - Public pages (landing, auth)
  • dashboard - Authenticated pages with sidebar

Route Structure

/                               # Landing page
/auth/login                     # Login
/auth/register                  # Registration
/auth/forgot-password           # Password reset request
/auth/reset-password            # Password reset form
/dashboard                      # Main dashboard
/dashboard/mail/*               # Email templates, blocks, media library
/dashboard/mail/media           # Media library
/dashboard/emails/*             # Email editor
/dashboard/campaigns/*          # Campaign management
/dashboard/audience/*           # Contacts, lists, segments
/dashboard/automations/*        # Automation workflows
/dashboard/transactional/*      # Transactional templates
/archive/:token                 # Campaign archive (public)
/dashboard/settings/*           # Organization settings

State Management

  • Convex queries provide reactive data
  • useAuth() - Authentication state
  • useCurrentOrganization() - Current organization context
  • useMediaLibrary() - Media asset management
  • useToast() - Global toast notifications
  • useFocusMode() - Editor focus mode
  • usePostHog() - Product analytics (capture events, identify users)
  • usePostHogIdentity() - Auto-syncs auth/org state to PostHog

Feature Flags

Some features are toggled via environment variables and can be enabled or disabled at runtime without code changes.

FeatureBackend env varFrontend env varPattern
AnalyticsPOSTHOG_API_KEYNUXT_PUBLIC_POSTHOG_API_KEYusePostHog() composable + lib/posthog.ts action

Backend helpers live in apps/api/convex/lib/. Frontend composables read from useRuntimeConfig().public and conditionally subscribe to Convex queries. The pattern:

  1. Backend: a small lib/*.ts file reads process.env and exports boolean helpers.
  2. Frontend: a useFeature() composable exposes reactive flags and conditional query subscriptions.
  3. Middleware: auth.ts and guest.ts check the composable and redirect as needed.
  4. UI: sidebar nav items and settings cards are conditionally rendered.