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/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)
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/email-previewerVorschaukomponenten für E-Mail-Clients (Kompatibilitätsanalyse, Can-I-Email-Daten)
packages/channelsNormalisierung eingehender Mail — eine quellenbasierte Registry (mta, resend, …), die den eingehenden Webhook-Envelope eines Anbieters in eine kanonische InboundEmailMessage überführt. Ausgehende Channel-Provider liegen nicht hier: Sie wohnen bei ihrem einzigen Aufrufer unter apps/api/convex/channels/adapters/
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 sendingsent 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. RegistryFEATURE_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ösungresolveFlags(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.