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
ResendEmail
AWS SESEmail
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)

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-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

When the waitlist feature is enabled, an additional gate is inserted:

Waitlist Flow
User SignupBetterAuth user created
userProfile createdwaitlistStatus: pending
Redirected to /waitlistHolding page
Admin approvesReal-time subscription fires → redirect to /dashboard
Auth middleware catches no orgRedirect to /setup/team → normal flow

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)
Email Provider (Resend/SES)Delivery
Webhooks → Status Updates → Analytics

Multi-tenancy

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.

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
mailingListsContact groupings
contactMailingListsMany-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
emailUsageCountersMetered usage tracking
contactActivitiesContact activity tracking

Background Jobs

The Convex backend runs scheduled cron jobs for background processing:

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
Flush email usage to Stripe5 minBatch meter events
Refresh segment counts30 minKeep cached counts fresh

Cron definitions live in apps/api/convex/crons.ts.

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
/waitlist                       # Waitlist holding page (when enabled)
/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
  • useWaitlist() - Waitlist feature flag and user status
  • 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
BillingBILLING_ENABLEDuseBilling() composable
WaitlistWAITLIST_ENABLEDNUXT_PUBLIC_WAITLIST_ENABLEDuseWaitlist() composable
AnalyticsPOSTHOG_API_KEYNUXT_PUBLIC_POSTHOG_API_KEYusePostHog() composable + lib/posthog.ts action

Backend helpers live in apps/api/convex/lib/ (e.g. waitlist.ts). 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.