Architekturüberblick

Owlat folgt einer modernen Serverless-Architektur mit Echtzeitfähigkeiten.

Owlat folgt einer modernen Serverless-Architektur mit Echtzeitfähigkeiten.

Systemarchitektur

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

Apps

AppBeschreibung
apps/webHaupt-Webanwendung (Nuxt 4 + Vue 3)
apps/apiBackend (Convex — Serverless-Funktionen, Datenbank, Auth)
apps/docsEntwicklerdokumentation (Nuxt Content)
apps/marketingMarketing- / Landingpage-Website (Nuxt)
apps/mtaAusgehender Mail Transfer Agent (Hono + GroupMQ + direktes SMTP, Endpunkt für Anhangs-Scans)
apps/imapIMAP4rev1-Server für die Postbox-Funktion (Port 993, implizites TLS — RFC 8314, kein STARTTLS), gestützt auf Convex
apps/mail-syncWorker, der sich nach außen mit den externen IMAP-/SMTP-Konten der Benutzer verbindet: synchronisiert eingehende Mail nahezu in Echtzeit (IMAP IDLE) und leitet ausgehende Sendungen weiter. Trägt die Tabellen externalMailAccounts / externalMailFolderSync
apps/setup-cliowlat-setup — Assistent, Verwaltung von Features/Packs/Env, Doctor-Prüfungen
apps/updaterIn-Place-Update-Sidecar, der Compose-Dateien neu schreibt und den Stack erneut deployt
apps/code-workerHintergrund-Worker, der die Queue codeWorkTasks abfragt und den OpenCode-Agenten gegen jede eingereihte Aufgabe ausführt (nur manuelle Aufgabenerstellung — die automatische Erstellung aus eingehender Mail ist nicht verdrahtet)
apps/convex-fn-proxySidecar mit minimalen Rechten vor Convex für den Code-Worker: hält den Deployment-Admin-Key und leitet NUR die freigegebenen codeWorkTasks/plugins/workerTasks-Funktionen des Workers weiter, damit die Sandbox nie einen Admin-Key besitzt
apps/desktopTauri-Desktop-Client (Multi-Workspace, Auth über den OS-Keychain, native Benachrichtigungen, Ungelesen-Badge in Dock/Taskleiste, Deep Links). Auto-Update ist nicht produktionsreif; die Distribution stellt der Betreiber bereit

Packages

PackageBeschreibung
packages/sharedGemeinsame Typen (Block-Typen, Editor-Typen, Kompatibilitätsdaten) und die Feature-Flag-Registry
packages/mta-protocolDer Wire-Contract zwischen Convex und MTA — Sendeannahme, Routing-Entscheidungen, der IP-Reputations-Snapshot und die Webhook-Events, jeweils einmal deklariert und von beiden Apps importiert
packages/email-builderVue-Komponenten des E-Mail-Builders (Notion-artiger Editor) und die Client-Vorschau unter src/preview/ (Kompatibilitätsanalyse, Can-I-Email-Daten)
packages/email-rendererRendering-Engine für E-Mail-HTML (tabellenbasiert, VML, CSS-Inlining)
packages/email-scannerSecurity-Scanning für E-Mails (Inhaltsanalyse, Dateivalidierung, URL-Reputation, ClamAV-Client)
packages/mail-messageHauseigene RFC-5322-/MIME-Nachrichtenbibliothek, zwei Hälften hinter einem Einstiegspunkt: der Parser (Header-Engine, Part-Baum, Charset-Dekodierung, Anhänge) und der Composer (reine RFC-5322-/RFC-2045-Konstruktion). Ersetzt mailparser eingehend und den MIME-Builder von nodemailer ausgehend
packages/mail-authHauseigene E-Mail-Authentifizierung — SPF (RFC 7208), DMARC (RFC 7489), DKIM-Verifikation (RFC 6376) sowie ein gecachter, injizierbarer DNS-Resolver
packages/mail-canonDer eine DKIM-Kanonisierer (RFC 6376 §3.4). Ein abhängigkeitsfreies Blatt, damit der eingehende Verifizierer und der ausgehende Signierer ihn ohne Build-Zyklus teilen können
packages/smtp-clientHauseigener SMTP-Client — die Wire-Rolle, die früher nodemailer innehatte. Reply-Parsing, ESMTP-Aushandlung, TLS und die Sendetransaktion
packages/smtp-listenerHauseigener SMTP-Listener — Kommandoschleife auf rohem Netz, Byte-Budget und Timeouts, hinter dem Port-25-MX- und dem Submission-Pfad. Ersetzt smtp-server
packages/provider-kitRuntime-neutrales universelles Send-Provider-Bundle, Trust- und Host-Verifier-Contracts, die Core- und Plugin-Provider gemeinsam nutzen
packages/plugin-kitÖffentliche Contracts zum Bau von Owlat-Plugins: das Manifest, Contribution-Buckets, Capabilities und die Modulformen pro Seam
packages/plugin-codegenValidierte Komposition zur Build-Zeit — prüft Package-Identität, Lockfile-Integrität und Realpath-Containment und gibt anschließend die generierten Kataloge und Registries aus
packages/plugin-hostRuntime-neutraler Durchsetzungskern für statisch komponierte Plugins: Flag-, Grant-, Umgebungs- und Scope-Autorisierung vor jedem Aufruf
packages/plugin-cliowlat plugins — create/add/remove/dev/codegen über die eingecheckte plugins.config.ts
packages/uiNuxt-Layer, der gemeinsame UI-Komponenten und Composables bereitstellt
packages/sdk-jsJavaScript-SDK für die Owlat-API
packages/sdk-javaJava-SDK für die Owlat-API (kein Workspace-Mitglied — keine package.json, mit Maven gebaut)

Beispiele

Ebenfalls Workspace-Mitglieder (examples/conformance und examples/plugins/*) und keine Dekoration: Die Referenz-Plugins gibt es eines pro Trust-Stufe, und die Conformance-Galerie treibt sie durch den ausgelieferten Host, Codegen und die CLI — so werden die Erweiterungspunkte der Plugin-Plattform durch etwas belegt, das weiterhin kompilieren muss.

BeispielBeschreibung
examples/conformanceDie Conformance-Galerie der Plugin-Plattform: Lifecycle- und Full-Pipeline-Replay-Suites, die die drei Referenz-Plugins durch den echten Host, Codegen und die CLI treiben, plus der Paritätsnachweis für Plugin-Provider — ein Fixture-ESP-Bundle, das durch die ausgelieferten Module für Routing, Dispatch, Messung, Return-Path und Credential-Formulare geführt wird
examples/plugins/escalation-guardReferenz-Plugin der Stufe 1: ein In-Process-Eskalationswächter, der einen Agent-Step, eine Draft-Strategie, einen Automations-Trigger/-Step/-Condition, ein Webhook-Event und gebündelte UI-Einträge beisteuert
examples/plugins/slack-approvalsReferenz-Connected-App der Stufe 2: ein reines Restrict-Hold-Gate für Slack-Freigaben über das signierte synchrone Hook-Protokoll
examples/plugins/deliverability-labProtokollreferenz der Stufe 3: Live-Pre-Send-Prüfungen hinter einem reinen Restrict-Gate plus ein Fixture für Seed-List-Worker-Requests; ein Host-Enqueue-Adapter wird nicht ausgeliefert

Datenfluss

Echtzeit-Updates

Convex liefert automatische Echtzeit-Updates:

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

// 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
    },
});

Authentifizierungsablauf

BenutzeranmeldungZugangsdaten übermittelt
BetterAuthAuthentifizierungs-Provider
Session erstelltJWT-Token ausgestellt
Organisation gesetztactiveOrganizationId in der Session gespeichert
Backend-Queries extrahieren organizationId aus der Session
Daten werden automatisch auf die Organisation eingegrenzt

Ablauf des E-Mail-Versands

Template erstelltJSON-Blöcke → @owlat/email-renderer → HTML
Domain hinzugefügtSES-Registrierung → DNS-Records generiert → Benutzer konfiguriert DNS
Kampagne erstelltZielgruppe ausgewählt → Domain verifiziert (DNS + SES)
Owlat MTA (Standard)Direktes SMTP · Intelligence-Pipeline · IP-Warming
Webhooks → Status-Updates → Analytics

Mandantenfähigkeit

Innerhalb einer Owlat-Instanz grenzt das Convex-Backend alle Daten über BetterAuth auf Organisationen ein:

// 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();

Authentifizierungsarchitektur

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

Architektur des E-Mail-Systems

Domain-Verwaltung

Die Domain-Verwaltung (Registrierung, DNS-Verifikation, Einrichtung der SES-Identität) erfolgt über die Dashboard-Oberfläche. Es gibt keine öffentliche API für die Domain-Verwaltung.

Überblick über das Datenbankschema

Das Convex-Schema (~97 Tabellen, aufgeteilt in Module pro Domäne unter apps/api/convex/schema/ und in defineSchema() hineingespreizt) folgt diesen Mustern:

Kerntabellen

TabelleZweck
userProfilesBenutzerprofildaten, verknüpft mit BetterAuth

Kontaktverwaltung

TabelleZweck
contactsE-Mail-Kontakte
contactPropertiesDefinitionen benutzerdefinierter Felder
contactPropertyValuesWerte benutzerdefinierter Felder pro Kontakt
topicsKontaktgruppierungen
contactTopicsMany-to-many mit DOI-Status
segmentsGespeicherte Filterkonfigurationen

E-Mail-System

TabelleZweck
emailTemplatesMarketing-/Transaktions-Templates
emailBlocksWiederverwendbare Inhaltsblöcke
campaignsEinmalige E-Mail-Sendungen (enthält archiveToken, archiveHtmlContent sowie die stats*-Familie — z. B. statsHardBounced, statsSoftBounced)
emailSendsTracking pro Empfänger
transactionalEmailsÜber die API ausgelöste Templates
transactionalSendsZustell-Tracking für Transaktionsmails

Automatisierungen

TabelleZweck
automationsWorkflow-Definitionen
automationStepsSchritte innerhalb von Workflows
automationRunsFortschritt eines Kontakts durch Workflows
automationStepRunsAusführung einzelner Schritte

Einstellungen & Sicherheit

TabelleZweck
domainsEigene Versanddomains
apiKeysAPI-Authentifizierung
webhooksEreignisbenachrichtigungen
webhookDeliveryLogsZustell-Tracking
blockedEmailsBlockliste für Bounces/Beschwerden
auditLogsAktionshistorie
formEndpointsKonfiguration öffentlicher Formulare
formSubmissionsDatensätze eingereichter Formulare
mediaAssetsDateien der Medienbibliothek
contactActivitiesTracking der Kontaktaktivität
instanceSettingsSchalter pro Deployment inkl. featureFlags

Postbox (persönliche Mail · Flag: postbox)

TabelleZweck
mailboxesPostfach pro Benutzer, gehostet oder external (ein BetterAuth-Benutzer kann viele besitzen)
externalMailAccountsZugangsdaten externer IMAP-/SMTP-Konten + Sync-Konfiguration, synchronisiert von apps/mail-sync
externalMailFolderSyncSync-Cursor pro Ordner für externe Konten
mailThreadsKonversationsgruppierung über Ordner hinweg
mailFoldersSystemordner (INBOX/Sent/Drafts/Trash/Spam/Archive) + Benutzerordner
mailMessagesEingehende/gesendete Nachrichten mit Envelope + Body-Parts (Snooze über das Feld snoozedUntil; ausgehender Zustand im Objekt outbound)
mailLabelsBenutzererstellte Labels (zusätzlich zu den Systemordnern)
mailDraftsEntwürfe im Editor (zugleich die Ausgangs-Queue — mail/outboundCron versendet geplante/ausstehende Entwürfe)
mailAliasesAdressaliase, die in ein Postfach routen
mailForwardingWeiterleitungsregeln
mailFiltersSieve-artige Regeln (Treffer → Aktionen)
mailSignaturesSignaturen pro Postfach
mailContactsAdressbuch pro Postfach
mailVacationRespondersZeitfenster für Abwesenheitsantworten
mailVacationLogAnti-Schleifen-Datensatz pro Absender für die Abwesenheitsantwort
mailAppPasswordsScrypt-gehashte Zugangsdaten für native IMAP-/SMTP-Clients
mailAuditLogEreignisprotokoll auf Postfachebene (Zustellung, IMAP-Login, …)
mailAuthFailuresProtokoll fehlgeschlagener Authentifizierungen im Gleitfenster, das das SMTP-Rate-Limit trägt

Die Schemabeziehungen, den IMAP-Kommandofluss und das Routing der eingehenden Zustellung finden Sie unter Postbox-Architektur.

Hintergrundjobs

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

Alle geplanten Jobs sind in apps/api/convex/crons.ts registriert — die maßgebliche Liste (34 Jobs). Eine repräsentative Auswahl:

JobIntervallZweck
Geplante Kampagnen verarbeiten1 Min.Absicherung für scheduler-basierte Sendungen
Sendende Kampagnen abgleichen1 Min.Sicherheitsnetz, das sending → sent fortschreibt, wenn keine eingereihten Sendungen mehr offen sind
Ausstehende Verzögerungen verarbeiten5 Min.Verpasste Verzögerungsschritte in Automatisierungen nachholen
Kontolöschungen verarbeiten24 StundenKonten nach Ablauf ihrer 30-tägigen Karenzzeit behandeln
Webhook-Logs aufräumen7 TageZustellprotokolle löschen, die älter als 30 Tage sind
Segmentzähler aktualisieren30 Min.Gecachte Segmentzähler frisch halten
Kontaktzähler abgleichen24 StundenDrift in gecachten Kontaktzählern korrigieren
Themen-Mitgliederzähler abgleichen24 StundenDrift in gecachten Themen-Mitgliederzählern korrigieren
Warming-Zustand synchronisieren5 Min.IP-Warming-Zustand vom MTA abholen
Sende-Reputation aufräumen1 StundeReputations-Buckets älter als 60 Tage verwerfen (das Risiko wird beim Lesen abgeleitet, ADR-0042)
Wartung des Knowledge Graph24 StundenKonfidenzabfall + Bereinigung abgelaufener Einträge
Fehlgeschlagene Agent-Aktionen wiederholen5 Min.Aktionen der Agent-Pipeline unterhalb des Retry-Limits erneut ausführen
Channel-Health-Checks5 Min.Konnektivität der Channels SMS / WhatsApp / generic prüfen
Rollup der Agent-Metriken5 Min.Queue-Tiefe, Latenz, Fehlerraten; Circuit Breaker auswerten
Tageszähler der Autonomie zurücksetzen24 StundenDie täglichen Autonomie-Aktionszähler zurücksetzen
Autonomie-Schwellwerte anpassen7 TageZieht eine Kategorie bei einem Ablehnungsanstieg automatisch enger; bei niedriger Ablehnungsquote wird ein Graduierungsvorschlag festgehalten (eine Lockerung erfordert die ausdrückliche Zustimmung des Benutzers, nie automatisch)
Analytics melden15 Min.Optional Instanzmetriken an eine externe Control Plane POSTen; ohne gesetzte CONTROL_PLANE_URL + INSTANCE_SECRET ein No-Op (wird mit dem OSS-Self-Host nicht ausgeliefert)
Zähler für Transaktionssendungen abgleichen24 StundenDrift im gecachten Zähler der Transaktionssendungen korrigieren
Postbox: überfällige Entwürfe versenden1 Min.Überfällige geplante/ausstehende Postbox-Entwürfe versenden, die der Scheduler pro Entwurf verpasst haben könnte
Postbox: zurückgestellte Nachrichten wecken1 Min.Nachrichten zurückholen, deren snoozedUntil verstrichen ist
Soft-gelöschte Kontakte aufräumen24 StundenKontakte nach Ablauf ihrer 30-tägigen Aufbewahrung endgültig löschen
Rollup der Statistiken gesendeter Kampagnen2 Min.Ereignisse pro Empfänger zu den stats*-Summen der Kampagne aggregieren
Rollup der Automatisierungsstatistiken1 Min.Zähler der Automatisierungs-Step-Runs aggregieren
Webhook-Payloads aufräumen7 TageGespeicherte Webhook-Payloads nach Ablauf der Aufbewahrung verwerfen
Aufbewahrung: Audit-Logs24 StundenAudit-Logs nach Ablauf ihres Aufbewahrungsfensters löschen
Aufbewahrung: Mail-Audit-Log24 StundenEinträge des Postfach-Audit-Logs nach Ablauf der Aufbewahrung löschen
Aufbewahrung: Metadaten von Formulareinreichungen24 StundenMetadaten von Formulareinreichungen nach Ablauf der Aufbewahrung entfernen
Aufbewahrung: fehlgeschlagene Mail-Authentifizierungen24 StundenSMTP-Auth-Fehlerdatensätze nach Ablauf der Aufbewahrung löschen
Reputationsbasierte Auto-Durchsetzung auswerten1 StundeReputationsbasierte Auto-Durchsetzungsmaßnahmen anwenden
Dedup im Knowledge Graph24 StundenDoppelte Entitäten im Knowledge Graph zusammenführen
Nicht verarbeitete Dateien nachziehen15 Min.Dateien verarbeiten, die noch nicht indexiert wurden
Feststeckende freigegebene Inbox-Nachrichten abgleichen5 Min.Freigegebene Inbox-Nachrichten weiterschieben, die in der Pipeline feststecken
Doppelte Kontakte automatisch zusammenführen6 StundenErkannte doppelte Kontakte zusammenführen
Single Source of Truth

Die obige Tabelle ist illustrativ. apps/api/convex/crons.ts registriert 34 Jobs und ist der maßgebliche, aktuelle Bestand — ziehen Sie diese Datei statt dieser Liste heran.

Noch nicht aktiv: graduierte Autonomie

Die Crons reset autonomy daily counts und adjust autonomy thresholds pflegen die Tabellen für Autonomieregeln und Feedback, aber die Entscheidungsschleife der graduierten Autonomie hat noch keine produktiven Aufrufer — live sind nur das Tageslimit des Agenten, der Konfidenz-Schwellwert und der Circuit Breaker llm_failure. Ebenso sind mehrere agentMetrics-Felder, die der Metrik-Rollup ausweist, noch nicht befüllt.

Frontend-Architektur

Layouts

  • default - Öffentliche Seiten (Landingpage, Auth)
  • dashboard - Authentifizierte Seiten mit Seitenleiste

Routen-Struktur

/                               # Landing page
/auth/login                     # Login
/auth/register                  # Registration
/auth/forgot-password           # Password reset request
/auth/reset-password            # Password reset form
/dashboard                      # Main dashboard
/dashboard/send/*               # Email templates, blocks, media library
/dashboard/send/media           # Media library
/dashboard/send/emails/*             # Email editor
/dashboard/campaigns/*          # Campaign management            (flag: campaigns)
/dashboard/automations/*        # Automation workflows            (flag: automations)
/dashboard/send/transactional/*      # Transactional templates         (flag: transactional)
/dashboard/audience/*           # Contacts, topics, segments
/dashboard/inbox/*              # Shared team inbox + triage      (flag: inbox)
/dashboard/chat/*               # Real-time chat                  (flag: chat)
/dashboard/postbox/*            # Personal mailbox webmail UI     (flag: postbox)
/dashboard/preferences/*   # Aliases, filters, app passwords (flag: postbox)
/dashboard/knowledge/*          # Knowledge graph                 (flag: ai.knowledge)
/dashboard/visualizations       # AI dashboards                   (flag: ai.visualizations)
/dashboard/files/*              # File browser
/archive/:token                 # Campaign archive (public)
/dashboard/admin/*           # Organization settings (incl. Features)

Seiten, die zu einem umschaltbaren Bereich gehören, deklarieren requiresFeature: '<flagKey>' in ihrem definePageMeta-Block. Die Auth-Middleware leitet auf einen Platzhalter „Feature deaktiviert“ um, wenn das Flag ausgeschaltet ist, und Convex-Queries erzwingen dieselbe Prüfung serverseitig.

State Management

  • Convex-Queries liefern reaktive Daten
  • useAuth() - Authentifizierungszustand
  • useOrganization() / useOrganizationContext() - Aktueller Organisationskontext
  • useMediaLibrary() - Verwaltung von Medien-Assets
  • useToast() - Globale Toast-Benachrichtigungen
  • useFocusMode() - Fokusmodus des Editors
  • usePostHog() - Produktanalytik (Events erfassen, Benutzer identifizieren)
  • usePostHogIdentity() - Synchronisiert Auth-/Org-Zustand automatisch zu PostHog

Feature-Flags

Jede umschaltbare Produktoberfläche wird genau einmal in packages/shared/src/featureFlags.ts deklariert — die Single Source of Truth, die das Setup-CLI, die Admin-Oberfläche, das Convex-Backend und die Nuxt-Route-Middleware lesen. Die vollständige Registry und die Helfer-APIs finden Sie unter Feature-Flags — Entwicklerreferenz.

Das Laufzeitmuster:

  1. Registry — FEATURE_FLAGS deklariert für jedes Flag Kategorie, Standardzustand, Abhängigkeiten (requires, cascadesOff), benötigte Umgebungsvariablen und das Docker-Compose-Profil.
  2. Speicherung — der aktuelle Zustand liegt in der Convex-Zeile instanceSettings.featureFlags.
  3. Auflösung — resolveFlags(stored) wendet die Abhängigkeitsregeln an und liefert den effektiven Zustand. Nutzen Sie das immer, bevor Sie ein Flag lesen.
  4. Aktivierung — ein Flag einzuschalten aktiviert sein Docker-Compose-Profil (von owlat-setup in die docker-compose.override.yml neu kompiliert) und validiert die dafür nötigen Umgebungsvariablen.
  5. Durchsetzung — die Page-Meta requiresFeature sperrt Routen; Convex-Queries rufen serverseitig isFlagEnabled() auf.

Provider

Wo Owlat mit einem externen System spricht, geschieht das über eine Provider-Factory, die an einer Umgebungsvariable hängt. Damit können Self-Hoster Implementierungen ohne Codeänderungen austauschen. Heute ist das für LLM und E-Mail-Versand verdrahtet; Benachrichtigungen und der Vector Store sind deklarierte Env-Schlüssel ohne bislang implementierte Factory.

SystemFactoryUmgebungsvariableImplementierungen
LLMlib/llmProvider.tsLLM_PROVIDERopenai (Standard), openrouter, ollama. Claude / jeder OpenAI-kompatible Endpunkt wird über LLM_BASE_URL erreicht, nicht über einen eigenen Provider-Wert
E-Mail-Versandproviders/composition.ts (universelle Bundles; Routing/Fallback in lib/sendProviders/routing.ts)EMAIL_PROVIDERmta, ses, resend, smtp, mandrill, emailit (die Kinds des Katalogs) — kein impliziter Standard; nicht gesetzt bedeutet „nicht konfiguriert“

Es gibt keine Env-Schlüssel für Notification-Provider oder Vector Store: Benachrichtigungen werden clientseitig von der Desktop-App erledigt, und der Knowledge-Abruf nutzt direkt den eingebauten Vektorindex von Convex (ctx.vectorSearch). Für Analytics kommt PostHog zum Einsatz, konfiguriert über POSTHOG_API_KEY / POSTHOG_HOST — es gibt weder eine Provider-Factory-Abstraktion noch einen ANALYTICS_PROVIDER-Schalter.

Interface-Contracts und die Vorgehensweise beim Hinzufügen eines neuen Providers finden Sie unter Provider.