Architecture overview
Status: this page describes the target architecture of Astela. Windows with tabs, the browser’s own pages, the data service, the content blocker and the overlay view exist; workspaces and tab groups are still design. See implementation status.
Astela is a desktop browser for Windows, macOS and Linux. It embeds Chromium through upstream Electron and is written in TypeScript. The browser process acts as a small kernel. Every feature is a module, every module beyond the core can be switched off, and a module that is off is never loaded. The principles explain why the architecture looks the way it does.
Key decisions
Section titled “Key decisions”Each decision is recorded in an architecture decision record.
| Topic | Decision | ADR |
|---|---|---|
| Engine | Upstream Electron, always on the latest stable major (45.x as of October 2026). Each new major is adopted within two to four weeks. | 0002 |
| Web content | BaseWindow with one WebContentsView per tab; no <webview> tag and no BrowserView. |
0002 |
| Language and build | TypeScript in strict mode, limited to erasable syntax; Vite with electron-vite builds the main, preload and renderer code. | 0004 |
| UI | Class-based web components with Lit, written without decorators; trivial leaf elements are plain HTMLElement classes. |
0005 |
| Dependency injection | A custom typed container with constructor injection and scopes, without reflection. | 0006 |
| IPC | Typed contracts with Valibot schemas, validated on every call. | 0007 |
| Internal pages | A privileged app:// scheme: the internal session serves it with protocol.registerSource, and every web session refuses it. |
0008 |
| Storage | node:sqlite in a utilityProcess, validated on every platform and architecture; a prebuilt native module stays the documented fallback. |
0009 |
| Modules | A microkernel with lazily loaded feature modules. | 0010 |
| Updates | electron-updater as the installation engine behind Astela’s own Ed25519-signed update feed. | 0014 |
Process model
Section titled “Process model”flowchart LR subgraph Browser["Browser process: Electron main, the kernel"] K["Kernel: DI, ModuleHost, EventBus, lifecycle, permission and navigation guards, app:// protocol"] WM["WindowManager and LayoutManager"] NET["RequestPipeline"] SET["Settings, policy and theme services"] UPD["UpdateService, electron-updater and BootGuard"] end subgraph Window["Each window: BaseWindow"] SH["Shell view: app://shell with toolbar, tabs and sidebar"] OV["Overlay view, lazy: menus, command palette, omnibox popup, prompts"] TV["Tab views: one WebContentsView per tab"] end DS[("utilityProcess data-service: SQLite, full-text search, favicons, import")] GPU[("GPU process")] NS[("Network service")] Browser <-->|typed IPC| SH Browser <-->|typed IPC| OV Browser -->|controls| TV Browser <-->|MessagePort| DS TV --- NS SH --- GPU- Browser process (Electron main): the kernel. It owns the lifecycle, windows, tabs (
WebContentsView), sessions and partitions, the network pipeline, settings, policies and updates. It stays lean: blocking work such as SQLite access, hashing and imports runs in autilityProcess. - Shell view (one renderer per window): the entire browser interface as class-based web components, loaded from the privileged scheme
app://shell. It runs sandboxed withcontextIsolationand a minimal preload that exposes a typed bridge, never a genericipcRenderer. - Overlay view (lazy): a transparent
WebContentsViewabove the web content for menus, the command palette, omnibox suggestions and permission prompts. It solves the z-order problem that web views always sit above the shell’s DOM. The overlay is created on first use and attached to the view tree only while something is open, because views are neither click-through nor focus-neutral. When it closes, the browser process returns focus to the page. Context menus are native (Menu.popup()). - Tab views: web content, sandboxed, with site isolation and without a preload; the one exception is the content blocker’s frame preload, which every web session registers for all its frames (ADR 0028). Feature scripts, such as the reader view, run only on demand in an isolated world. Mouse gestures use input events in the browser process instead of scripts injected into the page.
- Internal pages (
app://settings,app://history,app://downloads,app://theme-editor,app://diagnostics,app://newtab): regular tabs with a capability-based preload. Each page receives only the IPC channels it needs. - Data service (
utilityProcess): SQLite, full-text search, favicons and import. It starts lazily and talks to the browser process over aMessagePort. See data. - Updates: electron-updater runs in the browser process with asynchronous I/O. Verifying the Ed25519 signature of the feed takes microseconds. See updates.
Always-on guards
Section titled “Always-on guards”Some protections belong to the kernel, not to a module, so they can never be switched off: the permission handlers, the navigation and window guards, and the app:// registration, each installed on every session. Even with every optional module disabled, or in safe mode, every sensitive request stays denied. Tests will cover both “module off, still denied” and “a web tab cannot open app://settings”. See security.
IPC at a glance
Section titled “IPC at a glance”- Every channel is a contract in
shared/contractswith a request schema, a response schema and the capability allowed to use it. IpcRouterin the browser process checks, in this order: that the sender is one of Astela’s own views (a registered shell, overlay or internal page, with anapp://origin, in the top-level frame), that the view holds the required capability, that the payload matches the schema, and that the per-channel rate limit is respected. Anything invalid is dropped and logged locally.- Each preload exposes only
invoke(channel, payload)andsubscribe(event), and only for the channels its view type may use. A typed client is generated from the contracts. - There is no synchronous IPC. State flows from the browser process to the UI as a snapshot followed by patches, batched once per frame.
See IPC for the full design.
Layers and enforced boundaries
Section titled “Layers and enforced boundaries”The source code is split into layers with a strict dependency direction. Paths are relative to app/src/.
flowchart TB UI["ui/ and modules/*/ui: renderer code for ui-kit, shell, overlay and pages"] --> SHARED PRE["preload/: one bridge per view type"] --> SHARED MOD["modules/*/main: feature modules in the browser process"] --> DOMAIN MOD --> KERNEL MOD --> SHARED MAIN["main/: composition root and bootstrap"] --> MOD & PLATFORM & DOMAIN & KERNEL PLATFORM["platform/: adapters for Electron, Node.js and each operating system"] --> DOMAIN PLATFORM --> KERNEL DOMAIN["domain/: entities, services and ports, free of Electron, Node.js and DOM"] --> KERNEL KERNEL["kernel/: DI, module system, events, lifecycle and logging API"] --> SHARED SHARED["shared/: types, IPC contracts, Result, IDs and pure utilities"]Dependencies only follow the direction of the arrows; nothing points back up. shared is pure and imports nothing from the other layers, and main/ is the only place where all parts are wired together.
Three mechanisms enforce these boundaries, so that a violation fails the build instead of waiting for a review:
- TypeScript project references. Each layer is its own
compositeproject:shared,kernel,domain,platform,modules-main,modules-ui,preload,uiandmain. An import from a project that is not referenced fails to compile. Each project also restricts itslibandtypes:domain,kernelandsharedsee neither DOM nor Node.js types, andui,modules-uiandpreloadsee no Node.js types. - dependency-cruiser rules.
- The layer direction shown above, with no cycles.
electronmay only be imported inplatform/,main/andpreload/.modules/Amay import frommodules/Bonly throughmodules/B/api.ui/never imports code that runs in the browser process.- Each layer has a list of forbidden packages.
- Lint rules such as
no-restricted-imports, which give fast feedback in the editor.
Modules at a glance
Section titled “Modules at a glance”Every feature is a module under modules/<id>/. Its manifest.ts holds static metadata: it is tiny, purely declarative and always bundled. The main/ and ui/ code lives in separate chunks that are loaded with import() only when the module is active, and other modules reach it only through its api/ folder. The core modules windows, tabs, navigation, settings, policy, themes, i18n, session, updates and diagnostics are always active; everything else can be switched off by setting or policy. A test will verify that the chunks of disabled modules are never requested. See modules.
Central classes and interfaces
Section titled “Central classes and interfaces”classDiagram direction LR class Container { +register(token, provider, lifetime) +resolve(token) T +createChild() Container +dispose() } class ModuleHost { +start() +enable(id) +disable(id) } class IFeatureModule { <<interface>> +activate(ctx) +deactivate() } class ModuleContext { +commands +ipc +settings +ui +subscriptions } class EventBus { +emit(evt) +on(type, handler) Disposable } class ISettingsService { <<interface>> +get(key) +set(key, value) +inspect(key) SettingInspection +onDidChange(handler) } class SettingsService class ConfigLayer { <<interface>> +read(key) } class IPolicyService { <<interface>> +getPolicy(name) +isLocked(key) bool +onDidChange(handler) } class IPolicyProvider { <<interface>> +load() PolicySet +watch(callback) } class WindowsRegistryPolicyProvider class MacManagedPrefsPolicyProvider class LinuxJsonPolicyProvider class IThemeService { <<interface>> +apply(themeId, scope) +importFile(path) +exportTheme(id) +preview(theme) } class IWindowManager { <<interface>> +open(options) BrowserWindowController +all() } class BrowserWindowController { +shell +overlay +layout } class ITabService { <<interface>> +open(url, options) Tab +activate(id) +move(id, target) +sleep(id) } class Tab { +id +workspaceId +groupId +pinned +state +entries } class TabViewHost { +attach(tab, window) +detach(tab) +destroy(tab) } class ITabLifecyclePolicy { <<interface>> +shouldSleep(tab, context) bool } class IWorkspaceService { <<interface>> +create(spec) +switchTo(id, window) +moveTab(tabId, workspaceId) } class ISessionStore { <<interface>> +snapshot() +restore() } class ICommandRegistry { <<interface>> +register(command) +execute(id, args) } class IKeybindingService { <<interface>> +resolve(input) Command } class IpcRouter { +handle(contract, handler) +use(middleware) } class RequestPipeline { +use(interceptor) } class IRequestInterceptor { <<interface>> +onBeforeRequest(details) Decision } class IUpdateService { <<interface>> +check() +download() +installOnQuit() +state } class IManifestSource { <<interface>> +fetch(channel) SignedManifest } class ISignatureVerifier { <<interface>> +verify(bytes, signature) bool } class IUpdateInstaller { <<interface>> +canHandle(installType) bool +stage(artifact) +apply() } class BootGuard { +beginBoot() +markHealthy() +needsRollback() bool } class IDatabase { <<interface>> +exec(sql) +transaction(work) } class MigrationRunner { +run(target) }
ModuleHost --> Container : child container per module ModuleHost --> IFeatureModule : activates IFeatureModule ..> ModuleContext : receives ModuleContext --> ICommandRegistry : registers commands ModuleContext --> IpcRouter : registers handlers ModuleContext --> ISettingsService : listens to settings SettingsService ..|> ISettingsService SettingsService --> ConfigLayer : ordered layers IPolicyService --> IPolicyProvider WindowsRegistryPolicyProvider ..|> IPolicyProvider MacManagedPrefsPolicyProvider ..|> IPolicyProvider LinuxJsonPolicyProvider ..|> IPolicyProvider IKeybindingService ..> ICommandRegistry : resolves to commands IWindowManager --> BrowserWindowController ITabService --> Tab ITabService --> TabViewHost ITabService --> ITabLifecyclePolicy IWorkspaceService --> ITabService IUpdateService --> IManifestSource IUpdateService --> ISignatureVerifier IUpdateService --> IUpdateInstaller IUpdateService --> BootGuard RequestPipeline --> IRequestInterceptor IDatabase <-- MigrationRunner| Type | Kind | Responsibility |
|---|---|---|
Container |
Class | Typed dependency injection: registers providers with a lifetime, resolves tokens, creates child containers and disposes what it created. |
ModuleHost |
Class | Reads the module manifests, resolves the active set from policy, settings and dependencies, and starts, enables and disables modules. |
IFeatureModule |
Interface | The contract every feature module implements: activate(ctx) and deactivate(). |
ModuleContext |
Class | What a module receives on activation. Commands, IPC handlers, settings listeners and UI slots registered through it are disposables in subscriptions. |
EventBus |
Class | Typed domain events such as TabActivated and SettingChanged. |
ISettingsService, SettingsService |
Interface, class | Reads, writes and inspects settings across ordered configuration layers and reports changes. inspect(key) returns the value, its source and its lock state. |
ConfigLayer |
Interface | One layer of the settings resolution order, such as defaults, user settings or a mandatory policy. |
IPolicyService |
Interface | The merged policy state: values, locks and change events. |
IPolicyProvider |
Interface | Loads policies from one operating system source and watches it for changes. |
WindowsRegistryPolicyProvider, MacManagedPrefsPolicyProvider, LinuxJsonPolicyProvider |
Classes | The policy providers for the Windows registry, macOS managed preferences and Linux JSON files. |
IThemeService |
Interface | Applies a theme to a scope (global or a workspace), imports, exports and previews themes. |
IWindowManager |
Interface | Opens windows and keeps track of them. |
BrowserWindowController |
Class | One window: a BaseWindow with its shell view, lazy overlay view and content layout. |
ITabService |
Interface | Opens, activates, moves and puts tabs to sleep. |
Tab |
Class | The domain entity for a tab: ID, workspace, group, pinned flag, lifecycle state and navigation entries. A tab is not a view. |
TabViewHost |
Class | Creates a tab’s WebContentsView when needed, attaches it to a window, detaches it and destroys it. |
ITabLifecyclePolicy |
Interface | A strategy that decides whether a tab may sleep in a given context. |
IWorkspaceService |
Interface | Creates workspaces, switches the workspace a window shows and moves tabs between workspaces. |
ISessionStore |
Interface | Takes session snapshots and restores them. |
ICommandRegistry |
Interface | Registers and executes commands. |
IKeybindingService |
Interface | Resolves keyboard input to a command. |
IpcRouter |
Class | Binds handlers to IPC contracts and runs the middleware chain in front of them. |
RequestPipeline, IRequestInterceptor |
Class, interface | An ordered chain of network request interceptors; each returns a decision for a request. |
IUpdateService |
Interface | Checks for, downloads and installs updates, and exposes the updater state. |
IManifestSource |
Interface | Fetches the signed manifest for an update channel, from the default feed or a mirror. |
ISignatureVerifier |
Interface | Verifies Ed25519 signatures on the feed index and manifests. |
IUpdateInstaller |
Interface | Stages and applies an update for one installation type, such as NSIS or AppImage. |
BootGuard |
Class | Counts start attempts after an update and decides whether to roll back to the previous version. |
IDatabase |
Interface | Runs SQL statements and transactions in the data service. |
MigrationRunner |
Class | Runs data migrations transactionally up to a target schema version. |
Design patterns
Section titled “Design patterns”Patterns are used where they solve a concrete problem, not for decoration.
| Pattern | Use |
|---|---|
| Microkernel and plugins | The kernel plus feature modules (see modules). |
| Dependency injection | A custom typed container with static inject lists and no reflection, about 200 lines of code. Scopes: app, window and module. |
| Ports and adapters | domain defines ports such as IFileSystem, IKeyStore, IRegistry and IClock; platform implements them. Tests use in-memory adapters. |
| Command | CommandRegistry: shortcuts, the command palette, menus and the toolbar trigger the same commands. Undo for “reopen closed tab”. |
| Observer and event bus | Typed domain events such as TabActivated and SettingChanged. |
| Chain of responsibility | RequestPipeline (policy URL filter → HTTPS upgrade → blocker → permissions), IPC middleware (sender check → schema validation → rate limit), settings layers. |
| Strategy | Tab sleep policies, update installers per package format, policy providers per operating system, search engines. |
| State machine | Tab lifecycle (unloaded → loading → live → sleeping → crashed) and updater (idle → checking → downloading → staged → applying). |
| Memento | Session snapshots of windows, workspaces, tabs and navigation entries. |
| Composite | The split view layout tree and tab groups. |
| Mediator | LayoutManager coordinates shell insets, the tab bar position, the sidebar and the content bounds. |
| Repository | HistoryRepository, BookmarkRepository and others on top of IDatabase. |
| Facade | platform/electron/* wraps the Electron APIs, so that Electron upgrades stay local. |
Windows, tabs and workspaces
Section titled “Windows, tabs and workspaces”- Window: a
BrowserWindowControllercombines aBaseWindow, the shell view, the lazy overlay view and aContentLayout. TheLayoutManagercomputes all view bounds from the tab bar position (top, bottom, left or right), the sidebar, the density, compact mode and full screen; the shell reports its insets. - Tab and view: a tab is a domain entity, not a view.
TabViewHostcreates theWebContentsViewonly when the tab is activated and destroys it when the tab goes to sleep. The navigation entries are kept (navigationHistory), so a page comes back with its history when it wakes up. - Workspace: a name, icon, color, theme, partition (shared or isolated), tabs, groups and pinned tabs. A window shows one workspace at a time. Switching swaps the visible views; tabs that are already live stay alive, so the switch is instant and nothing reloads.
- Tab groups (name, color, collapsible) and pinned tabs belong to their workspace.
- Partitions:
persist:defaultfor shared workspaces,persist:ws-<id>for each isolated workspace, and an in-memory partition for private windows.
Drag and drop
Section titled “Drag and drop”- Within a window: pointer events in the shell handle the drag.
- Between windows and onto workspace icons: the browser process runs a drag session. The source shell reports screen coordinates through pointer capture, and the browser process finds the target window and insertion index with a hit test.
- Moving without reload: the
WebContentsViewis re-attached to the target window without a reload, so page state such as the scroll position and form content survives. - Tear-off: dropping a tab outside every window opens a new window.
- Isolated workspaces: moving a tab into a workspace with a different partition reloads it in the target partition, and the UI indicates this unobtrusively.
sequenceDiagram participant S as Source shell participant M as Browser process participant T as Target window S->>M: Start a drag session for a tab loop While the pointer moves S->>M: Pointer position in screen coordinates M->>M: Hit-test target window and insertion index end alt Dropped over a window M->>T: Re-attach the WebContentsView without reload M-->>T: Patch the tab strip state else Dropped outside every window M->>M: Open a new window and attach the view there end M-->>S: Remove the tab from the source tab stripWayland
Section titled “Wayland”Wayland does not let applications position their windows. A torn-off window is therefore placed by the compositor rather than at the pointer position, and a setting to run under X11 instead is planned. The overlay view is transparent on every system; its rendering under Wayland is not yet covered by the tests, which run under X11.
Further reading
Section titled “Further reading”- Principles: the eight principles, target groups and non-goals.
- Modules: the module system, manifests and lazy loading.
- IPC: contracts, validation and state flow between processes.
- Data: storage, atomic writes, backups and migrations.
- Settings, policies and themes: one schema for the UI, policies and documentation.
- Design system: tokens, density, typography, icons and motion.
- Enterprise: policy sources, MSI, proxy, certificates, SSO and diagnostics.
- Updates: the signed feed, per-format update strategies and rollback.
- Security: Electron hardening, permissions, network protections and the supply chain.
- Performance: budgets, techniques and the measurement method.
- Testing: test levels, tools and rules.
- Validation results: measurements from the experiments that tested the riskiest assumptions.
- Architecture decision records: every significant decision and its reasoning.
- Roadmap: milestones, priorities and the M1 delivery order.
Implementation status
Section titled “Implementation status”The first browser window works end to end:
- Process model: one
BaseWindowper window with a shell view (app://shell) and oneWebContentsViewper tab, created when the tab first becomes active. A window appears at once in the background color of the theme and the shell paints into it; a hidden window would load the shell at a lower priority. Web content and the browser’s own pages use separate sessions. New tabs open on the new tab page (app://newtab/), an internal page in a view of the internal session with a search field for the chosen search engine. When a tab switches between a web page and an internal page, its view is replaced and the old view’s history is kept, so Back from the first web page returns to the new tab page (ADR 0026). The first start of a profile opens the setup guide (app://setup) instead of the new tab page, once per profile. The menu button at the end of the toolbar opens the browser menu, a native menu with new tabs and windows, the history, downloads and bookmarks, the page tools, the settings, About and Quit; the settings page (app://settings) opens in the tab that shows it already, in place of a new tab page, or in a new tab. - Shell: the tab strip at the top or bottom or as a list on the left or right, back, forward and reload buttons, an address bar that tells URLs from searches, the bookmark star, the content blocker’s shield, the menu button and the optional bookmarks bar (
app/src/ui/shell/). The keyboard shortcuts for tabs, history, reload, the address bar and the page tools follow the conventions of each system and work in the shell and in web content (app/src/platform/electron/shortcuts.ts). The browser process sees each key before the page, so no default shortcut is a key that edits text. The back and forward buttons of a mouse work over the page and over the toolbar. - Page tools: the find bar below the toolbar looks for text with Chromium’s
findInPageand shows the number of matches. The zoom of a page applies to its whole site, also in other tabs, and is kept in the settingzoom.sites; the address bar shows it while it is not 100 %. When a page shows an element in full screen, its view covers the window. Printing, saving, the source and the developer tools load with their first use (app/src/main/page-tools.ts); they work on web pages, not on the browser’s own pages, and the policyDeveloperToolsAvailabilitycan keep the developer tools closed. Chromium’s PDF viewer shows PDF documents in the tab. - Composition root:
app/src/main/bootstrap.tswires the first services by hand. It moves onto the dependency injection container and the module host with the first feature module. - Context menus: native menus (
Menu.popup()) for links, images, selected text, text fields and the page itself, and for the address bar; see the user guide. A pure function inapp/src/domain/context-menu/describes each menu from Chromium’s context menu parameters, the texts and the platform, and has unit tests.app/src/main/context-menu.tsshows the description and carries out the commands on the view that was clicked; it loads with the first context menu, not before the first window. The texts come from the browser process’s own catalogs inapp/src/main/i18n/, which ship the same languages as the shell, follow the system language like the shell and share its translator inapp/src/shared/i18n/. They mark an access key for Windows and Linux, and macOS gets English in title case. Tabs opened from a menu take the same path and the same http and https rule as tabs that pages open; tabs opened in the background line up after the page in the order they were opened. The menus of web pages end with Inspect, which opens the developer tools at the clicked element, unless the policyDeveloperToolsAvailabilitykeeps them closed. The View menu of the macOS menu bar runs the same commands as the keys, so it acts on the page and follows the policy too. - Data service: a utility process owns
profile.dbwith the history and the bookmarks and runs the import from other browsers; it starts with the first data access: the first web page, the bookmarks bar, the history or bookmarks page, or the list of other browsers in the settings and the setup guide. See data storage and ADR 0029. - Windows and popups: what a script of a page opens keeps its opener: a tab next to the page, or for a popup with a size a window of its own;
Ctrl+Nopens a new window (ADR 0040). Private windows (Ctrl+Shift+N) load web pages in a session in memory and keep no history or session (ADR 0041). - Session: the windows, their tabs and where the windows were are kept in
session.jsonand restored at the next start; restored tabs get their view when they are first shown. - Content blocker: one request pipeline per web session blocks requests with the filter engine, and a frame preload applies element hiding rules and scriptlets in every frame (ADR 0027, ADR 0028).
- Overlay: a transparent view above the tabs shows popovers and prompts: the content blocker’s popover, the sign-in prompt, the questions of sites for permissions and the screen picker; it is part of the window only while it is open (ADR 0034).
- Downloads: web sessions save downloads in the downloads folder without a question, under a name that is free there (
app/src/main/bootstrap.ts); the list of downloads, kept inprofile.db, its panel in the overlay and the progress of the downloads button load with the first download (app/src/main/downloads.ts, ADR 0039). - Site permissions: web sessions ask before a site uses the camera, the microphone, the location, notifications or the clipboard, keep the answer per origin in the setting
permissions.sitesand list it on the settings page; the prompts load with the first question (app/src/main/site-permissions.ts,app/src/main/permission-prompt.ts, ADR 0038). - Links from other apps: links on the command line, from a second start and from
open-urlevents open in tabs; the Windows installer registers the browser for web links (ADR 0030).
Not implemented yet: workspaces, tab groups and drag and drop.