Architecture Overview
Owlat follows a modern serverless architecture with real-time capabilities.
Owlat follows a modern serverless architecture with real-time capabilities.
System Architecture
Monorepo Structure
Apps
| App | Description |
|---|---|
apps/web | Main web application (Nuxt 4 + Vue 3) |
apps/api | Backend (Convex — serverless functions, database, auth) |
apps/docs | Developer documentation (VitePress) |
apps/marketing | Marketing / landing page site (Nuxt) |
Packages
| Package | Description |
|---|---|
packages/shared | Shared types (block types, editor types, compatibility data) |
packages/email-builder | Email builder Vue components (Notion-like editor) |
packages/email-renderer | Email HTML rendering engine (table-based, VML, CSS inlining) |
packages/email-previewer | Email client preview components (compatibility analysis, Can I Email data) |
packages/ui | Nuxt layer providing shared UI components and composables |
packages/sdk-js | JavaScript SDK for the Owlat API |
packages/sdk-java | Java 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
When the waitlist feature is enabled, an additional gate is inserted:
Email Sending Flow
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
- Stores user/session data in Convex
- Provides auth routes via HTTP handlers
- Links sessions to organizations
Email System Architecture
sesIdentity.ts- Domain registration (VerifyDomainIdentity/DKIM)
- MAIL FROM configuration
- Verification status polling
getEmailProvider()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
| Table | Purpose |
|---|---|
userProfiles | User profile data linked to BetterAuth |
organizationSettings | Organization-level configuration |
Contact Management
| Table | Purpose |
|---|---|
contacts | Email contacts |
contactProperties | Custom field definitions |
contactPropertyValues | Custom field values per contact |
mailingLists | Contact groupings |
contactMailingLists | Many-to-many with DOI status |
segments | Saved filter configurations |
Email System
| Table | Purpose |
|---|---|
emailTemplates | Marketing/transactional templates |
emailBlocks | Reusable content blocks |
campaigns | One-time email sends (includes archiveToken, archiveHtml, hardBounces, softBounces) |
emailSends | Per-recipient tracking |
transactionalEmails | API-triggered templates |
transactionalSends | Transactional delivery tracking |
Automations
| Table | Purpose |
|---|---|
automations | Workflow definitions |
automationSteps | Steps within workflows |
automationRuns | Contact progress through workflows |
automationStepRuns | Individual step execution |
Settings & Security
| Table | Purpose |
|---|---|
domains | Custom sending domains |
apiKeys | API authentication |
webhooks | Event notifications |
webhookDeliveryLogs | Delivery tracking |
blockedEmails | Bounce/complaint blocklist |
auditLogs | Action history |
formEndpoints | Public form configuration |
formSubmissions | Form submission records |
mediaAssets | Media library files |
emailUsageCounters | Metered usage tracking |
contactActivities | Contact activity tracking |
Background Jobs
The Convex backend runs scheduled cron jobs for background processing:
| Job | Interval | Purpose |
|---|---|---|
| Process scheduled campaigns | 1 min | Backup for scheduler-based sends |
| Process pending delays | 5 min | Catch missed automation delays |
| Process account deletions | 24 hours | Handle expired grace periods |
| Cleanup webhook logs | 7 days | Remove logs older than 30 days |
| Flush email usage to Stripe | 5 min | Batch meter events |
| Refresh segment counts | 30 min | Keep 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 stateuseCurrentOrganization()- Current organization contextuseWaitlist()- Waitlist feature flag and user statususeMediaLibrary()- Media asset managementuseToast()- Global toast notificationsuseFocusMode()- Editor focus modeusePostHog()- 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.
| Feature | Backend env var | Frontend env var | Pattern |
|---|---|---|---|
| Billing | BILLING_ENABLED | — | useBilling() composable |
| Waitlist | WAITLIST_ENABLED | NUXT_PUBLIC_WAITLIST_ENABLED | useWaitlist() composable |
| Analytics | POSTHOG_API_KEY | NUXT_PUBLIC_POSTHOG_API_KEY | usePostHog() 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:
- Backend: a small
lib/*.tsfile readsprocess.envand exports boolean helpers. - Frontend: a
useFeature()composable exposes reactive flags and conditional query subscriptions. - Middleware:
auth.tsandguest.tscheck the composable and redirect as needed. - UI: sidebar nav items and settings cards are conditionally rendered.