Architekturüberblick
Owlat folgt einer modernen Serverless-Architektur mit Echtzeitfähigkeiten.
Owlat folgt einer modernen Serverless-Architektur mit Echtzeitfähigkeiten.
Systemarchitektur
Monorepo-Struktur
Apps
| App | Beschreibung |
|---|---|
apps/web | Haupt-Webanwendung (Nuxt 4 + Vue 3) |
apps/api | Backend (Convex — Serverless-Funktionen, Datenbank, Auth) |
apps/docs | Entwicklerdokumentation (Nuxt Content) |
apps/marketing | Marketing- / Landingpage-Website (Nuxt) |
apps/mta | Ausgehender Mail Transfer Agent (Hono + GroupMQ + direktes SMTP, Endpunkt für Anhangs-Scans) |
apps/imap | IMAP4rev1-Server für die Postbox-Funktion (Port 993, implizites TLS — RFC 8314, kein STARTTLS), gestützt auf Convex |
apps/mail-sync | Worker, 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-cli | owlat-setup — Assistent, Verwaltung von Features/Packs/Env, Doctor-Prüfungen |
apps/updater | In-Place-Update-Sidecar, der Compose-Dateien neu schreibt und den Stack erneut deployt |
apps/code-worker | Hintergrund-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/desktop | Tauri-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
| Package | Beschreibung |
|---|---|
packages/shared | Gemeinsame Typen (Block-Typen, Editor-Typen, Kompatibilitätsdaten) und die Feature-Flag-Registry |
packages/mta-protocol | Der 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-builder | Vue-Komponenten des E-Mail-Builders (Notion-artiger Editor) |
packages/email-renderer | Rendering-Engine für E-Mail-HTML (tabellenbasiert, VML, CSS-Inlining) |
packages/email-scanner | Security-Scanning für E-Mails (Inhaltsanalyse, Dateivalidierung, URL-Reputation, ClamAV-Client) |
packages/email-previewer | Vorschaukomponenten für E-Mail-Clients (Kompatibilitätsanalyse, Can-I-Email-Daten) |
packages/channels | Normalisierung 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-message | Hauseigene 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-auth | Hauseigene E-Mail-Authentifizierung — SPF (RFC 7208), DMARC (RFC 7489), DKIM-Verifikation (RFC 6376) sowie ein gecachter, injizierbarer DNS-Resolver |
packages/mail-canon | Der 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-client | Hauseigener SMTP-Client — die Wire-Rolle, die früher nodemailer innehatte. Reply-Parsing, ESMTP-Aushandlung, TLS und die Sendetransaktion |
packages/smtp-listener | Hauseigener SMTP-Listener — Kommandoschleife auf rohem Netz, Byte-Budget und Timeouts, hinter dem Port-25-MX- und dem Submission-Pfad. Ersetzt smtp-server |
packages/provider-kit | Runtime-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-codegen | Validierte 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-host | Runtime-neutraler Durchsetzungskern für statisch komponierte Plugins: Flag-, Grant-, Umgebungs- und Scope-Autorisierung vor jedem Aufruf |
packages/plugin-cli | owlat plugins — create/add/remove/dev/codegen über die eingecheckte plugins.config.ts |
packages/ui | Nuxt-Layer, der gemeinsame UI-Komponenten und Composables bereitstellt |
packages/sdk-js | JavaScript-SDK für die Owlat-API |
packages/sdk-java | Java-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.
| Beispiel | Beschreibung |
|---|---|
examples/conformance | Die 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-guard | Referenz-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-approvals | Referenz-Connected-App der Stufe 2: ein reines Restrict-Hold-Gate für Slack-Freigaben über das signierte synchrone Hook-Protokoll |
examples/plugins/deliverability-lab | Protokollreferenz 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
Ablauf des E-Mail-Versands
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
- Stores user/session data in Convex
- Provides auth routes via HTTP handlers
- Links sessions to organizations
Architektur des E-Mail-Systems
sesIdentity.ts- Domain registration (VerifyDomainIdentity/DKIM)
- MAIL FROM configuration
- Verification status polling
resolveRoute() → getProviderByType()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
| Tabelle | Zweck |
|---|---|
userProfiles | Benutzerprofildaten, verknüpft mit BetterAuth |
Kontaktverwaltung
| Tabelle | Zweck |
|---|---|
contacts | E-Mail-Kontakte |
contactProperties | Definitionen benutzerdefinierter Felder |
contactPropertyValues | Werte benutzerdefinierter Felder pro Kontakt |
topics | Kontaktgruppierungen |
contactTopics | Many-to-many mit DOI-Status |
segments | Gespeicherte Filterkonfigurationen |
E-Mail-System
| Tabelle | Zweck |
|---|---|
emailTemplates | Marketing-/Transaktions-Templates |
emailBlocks | Wiederverwendbare Inhaltsblöcke |
campaigns | Einmalige E-Mail-Sendungen (enthält archiveToken, archiveHtmlContent sowie die stats*-Familie — z. B. statsHardBounced, statsSoftBounced) |
emailSends | Tracking pro Empfänger |
transactionalEmails | Über die API ausgelöste Templates |
transactionalSends | Zustell-Tracking für Transaktionsmails |
Automatisierungen
| Tabelle | Zweck |
|---|---|
automations | Workflow-Definitionen |
automationSteps | Schritte innerhalb von Workflows |
automationRuns | Fortschritt eines Kontakts durch Workflows |
automationStepRuns | Ausführung einzelner Schritte |
Einstellungen & Sicherheit
| Tabelle | Zweck |
|---|---|
domains | Eigene Versanddomains |
apiKeys | API-Authentifizierung |
webhooks | Ereignisbenachrichtigungen |
webhookDeliveryLogs | Zustell-Tracking |
blockedEmails | Blockliste für Bounces/Beschwerden |
auditLogs | Aktionshistorie |
formEndpoints | Konfiguration öffentlicher Formulare |
formSubmissions | Datensätze eingereichter Formulare |
mediaAssets | Dateien der Medienbibliothek |
contactActivities | Tracking der Kontaktaktivität |
instanceSettings | Schalter pro Deployment inkl. featureFlags |
Postbox (persönliche Mail · Flag: postbox)
| Tabelle | Zweck |
|---|---|
mailboxes | Postfach pro Benutzer, gehostet oder external (ein BetterAuth-Benutzer kann viele besitzen) |
externalMailAccounts | Zugangsdaten externer IMAP-/SMTP-Konten + Sync-Konfiguration, synchronisiert von apps/mail-sync |
externalMailFolderSync | Sync-Cursor pro Ordner für externe Konten |
mailThreads | Konversationsgruppierung über Ordner hinweg |
mailFolders | Systemordner (INBOX/Sent/Drafts/Trash/Spam/Archive) + Benutzerordner |
mailMessages | Eingehende/gesendete Nachrichten mit Envelope + Body-Parts (Snooze über das Feld snoozedUntil; ausgehender Zustand im Objekt outbound) |
mailLabels | Benutzererstellte Labels (zusätzlich zu den Systemordnern) |
mailDrafts | Entwürfe im Editor (zugleich die Ausgangs-Queue — mail/outboundCron versendet geplante/ausstehende Entwürfe) |
mailAliases | Adressaliase, die in ein Postfach routen |
mailForwarding | Weiterleitungsregeln |
mailFilters | Sieve-artige Regeln (Treffer → Aktionen) |
mailSignatures | Signaturen pro Postfach |
mailContacts | Adressbuch pro Postfach |
mailVacationResponders | Zeitfenster für Abwesenheitsantworten |
mailVacationLog | Anti-Schleifen-Datensatz pro Absender für die Abwesenheitsantwort |
mailAppPasswords | Scrypt-gehashte Zugangsdaten für native IMAP-/SMTP-Clients |
mailAuditLog | Ereignisprotokoll auf Postfachebene (Zustellung, IMAP-Login, …) |
mailAuthFailures | Protokoll 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:
| Job | Intervall | Zweck |
|---|---|---|
| Geplante Kampagnen verarbeiten | 1 Min. | Absicherung für scheduler-basierte Sendungen |
| Sendende Kampagnen abgleichen | 1 Min. | Sicherheitsnetz, das sending → sent fortschreibt, wenn keine eingereihten Sendungen mehr offen sind |
| Ausstehende Verzögerungen verarbeiten | 5 Min. | Verpasste Verzögerungsschritte in Automatisierungen nachholen |
| Kontolöschungen verarbeiten | 24 Stunden | Konten nach Ablauf ihrer 30-tägigen Karenzzeit behandeln |
| Webhook-Logs aufräumen | 7 Tage | Zustellprotokolle löschen, die älter als 30 Tage sind |
| Segmentzähler aktualisieren | 30 Min. | Gecachte Segmentzähler frisch halten |
| Kontaktzähler abgleichen | 24 Stunden | Drift in gecachten Kontaktzählern korrigieren |
| Themen-Mitgliederzähler abgleichen | 24 Stunden | Drift in gecachten Themen-Mitgliederzählern korrigieren |
| Warming-Zustand synchronisieren | 5 Min. | IP-Warming-Zustand vom MTA abholen |
| Sende-Reputation aufräumen | 1 Stunde | Reputations-Buckets älter als 60 Tage verwerfen (das Risiko wird beim Lesen abgeleitet, ADR-0042) |
| Wartung des Knowledge Graph | 24 Stunden | Konfidenzabfall + Bereinigung abgelaufener Einträge |
| Fehlgeschlagene Agent-Aktionen wiederholen | 5 Min. | Aktionen der Agent-Pipeline unterhalb des Retry-Limits erneut ausführen |
| Channel-Health-Checks | 5 Min. | Konnektivität der Channels SMS / WhatsApp / generic prüfen |
| Rollup der Agent-Metriken | 5 Min. | Queue-Tiefe, Latenz, Fehlerraten; Circuit Breaker auswerten |
| Tageszähler der Autonomie zurücksetzen | 24 Stunden | Die täglichen Autonomie-Aktionszähler zurücksetzen |
| Autonomie-Schwellwerte anpassen | 7 Tage | Zieht 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 melden | 15 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 abgleichen | 24 Stunden | Drift im gecachten Zähler der Transaktionssendungen korrigieren |
| Postbox: überfällige Entwürfe versenden | 1 Min. | Überfällige geplante/ausstehende Postbox-Entwürfe versenden, die der Scheduler pro Entwurf verpasst haben könnte |
| Postbox: zurückgestellte Nachrichten wecken | 1 Min. | Nachrichten zurückholen, deren snoozedUntil verstrichen ist |
| Soft-gelöschte Kontakte aufräumen | 24 Stunden | Kontakte nach Ablauf ihrer 30-tägigen Aufbewahrung endgültig löschen |
| Rollup der Statistiken gesendeter Kampagnen | 2 Min. | Ereignisse pro Empfänger zu den stats*-Summen der Kampagne aggregieren |
| Rollup der Automatisierungsstatistiken | 1 Min. | Zähler der Automatisierungs-Step-Runs aggregieren |
| Webhook-Payloads aufräumen | 7 Tage | Gespeicherte Webhook-Payloads nach Ablauf der Aufbewahrung verwerfen |
| Aufbewahrung: Audit-Logs | 24 Stunden | Audit-Logs nach Ablauf ihres Aufbewahrungsfensters löschen |
| Aufbewahrung: Mail-Audit-Log | 24 Stunden | Einträge des Postfach-Audit-Logs nach Ablauf der Aufbewahrung löschen |
| Aufbewahrung: Metadaten von Formulareinreichungen | 24 Stunden | Metadaten von Formulareinreichungen nach Ablauf der Aufbewahrung entfernen |
| Aufbewahrung: fehlgeschlagene Mail-Authentifizierungen | 24 Stunden | SMTP-Auth-Fehlerdatensätze nach Ablauf der Aufbewahrung löschen |
| Reputationsbasierte Auto-Durchsetzung auswerten | 1 Stunde | Reputationsbasierte Auto-Durchsetzungsmaßnahmen anwenden |
| Dedup im Knowledge Graph | 24 Stunden | Doppelte Entitäten im Knowledge Graph zusammenführen |
| Nicht verarbeitete Dateien nachziehen | 15 Min. | Dateien verarbeiten, die noch nicht indexiert wurden |
| Feststeckende freigegebene Inbox-Nachrichten abgleichen | 5 Min. | Freigegebene Inbox-Nachrichten weiterschieben, die in der Pipeline feststecken |
| Doppelte Kontakte automatisch zusammenführen | 6 Stunden | Erkannte doppelte Kontakte zusammenführen |
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.
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()- AuthentifizierungszustanduseOrganization()/useOrganizationContext()- Aktueller OrganisationskontextuseMediaLibrary()- Verwaltung von Medien-AssetsuseToast()- Globale Toast-BenachrichtigungenuseFocusMode()- Fokusmodus des EditorsusePostHog()- 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:
- Registry —
FEATURE_FLAGSdeklariert für jedes Flag Kategorie, Standardzustand, Abhängigkeiten (requires,cascadesOff), benötigte Umgebungsvariablen und das Docker-Compose-Profil. - Speicherung — der aktuelle Zustand liegt in der Convex-Zeile
instanceSettings.featureFlags. - Auflösung —
resolveFlags(stored)wendet die Abhängigkeitsregeln an und liefert den effektiven Zustand. Nutzen Sie das immer, bevor Sie ein Flag lesen. - Aktivierung — ein Flag einzuschalten aktiviert sein Docker-Compose-Profil (von
owlat-setupin diedocker-compose.override.ymlneu kompiliert) und validiert die dafür nötigen Umgebungsvariablen. - Durchsetzung — die Page-Meta
requiresFeaturesperrt Routen; Convex-Queries rufen serverseitigisFlagEnabled()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.
| System | Factory | Umgebungsvariable | Implementierungen |
|---|---|---|---|
| LLM | lib/llmProvider.ts | LLM_PROVIDER | openai (Standard), openrouter, ollama. Claude / jeder OpenAI-kompatible Endpunkt wird über LLM_BASE_URL erreicht, nicht über einen eigenen Provider-Wert |
| E-Mail-Versand | providers/composition.ts (universelle Bundles; Routing/Fallback in lib/sendProviders/routing.ts) | EMAIL_PROVIDER | mta, 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.