bDS2
bDS2 is the Elixir rewrite of bDS, the offline-first desktop blogging workspace. It is no longer just a rewrite scaffold: the repository now contains the main desktop runtime, Ecto persistence, filesystem-backed content workflows, rendering and publishing pipelines, Lua scripting, AI and MCP integration, and a Phoenix LiveView shell embedded in a native desktop window.
The Allium specs in specs/ remain the behavioral contract for the rewrite. For end-user operation, see DOCUMENTATION.md. For the scripting surface, see API.md.
Current Status
The major architectural rework is in place.
- The desktop UI is served by Phoenix LiveView inside the desktop shell rather than by a separate handwritten frontend runtime.
- Assets use Phoenix-default Tailwind and esbuild tooling from assets/ into priv/static/.
- Core editorial flows are implemented in the main application: posts, media, tags, templates, scripts, imports, preview, generation, publishing, maintenance, AI, and MCP.
- Localization is now a first-class architectural concern rather than an afterthought: UI chrome and rendered site output have separate locale flows, and post/media translation workflows are built into the domain model.
- The app boots in three modes: the desktop app, a headless server (SSH-served terminal UI + tunneled GUI), and a local TUI — see Headless Server Mode And Terminal UI.
The rewrite still aims to preserve the product behavior of bDS while replacing the technical stack. The contract is product behavior, not the old implementation language or framework choices.
Architecture Overview
Runtime
BDS.Application is the supervision root. It starts the Phoenix endpoint, database, preview and publishing workers, task supervisors, scripting jobs, and the desktop server/window adapters.
At a high level, the stack is:
- Native windowing through the
:desktopintegration. - Phoenix endpoint and LiveView shell for the actual app UI.
- Ecto + SQLite for indexed state, editor state, and app data.
- Filesystem-backed project data for published content, media, sidecars, scripts, templates, generated output, and rebuild workflows.
Desktop Shell
The desktop workbench lives under lib/bds/desktop/. The main screen is BDS.Desktop.ShellLive, with feature-specific editors and sidebar logic under lib/bds/desktop/shell_live/.
If you are tracing UI behavior, start there first:
- LiveView event routing, workbench state, overlays, and menu handling live in the desktop shell modules.
- HEEx templates under the same tree now own most common layout and state styling.
- Monaco is bundled as an ESM bundle via esbuild into
priv/static/assets/monaco.jsandmonaco.css(source entry atassets/js/monaco_entry.js).
Domain Modules
Most application behavior lives under lib/bds/:
- posts, media, tags, templates, scripts, and project settings
- metadata, frontmatter, sidecars, rebuild, and maintenance
- rendering, generation, preview, and publishing
- AI runtimes, chat tooling, embeddings, and MCP
- scripting capabilities and generated API docs
The repo has been pushed toward smaller feature-focused modules rather than one large mixed runtime. For new work, prefer finding the owning feature module instead of adding more behavior to broad catch-all files.
Storage Model
The database is important, but it is not the whole source of truth.
- Ecto models hold app state, indexes, editor state, and workflow data.
- The filesystem holds published content artifacts and sidecar metadata that must stay stable and reviewable.
- Rebuild and metadata-diff flows exist because database state and filesystem state are expected to stay in sync.
When you change persisted behavior, think in both directions: database writes and filesystem writes/readback.
Localization And i18n
Localization now has two separate layers, and confusing them causes bugs.
1. UI Localization
UI chrome, menus, dashboard text, editor labels, and toasts use Gettext through BDS.Gettext and the ui domain. Locale normalization lives in BDS.I18n, and the desktop shell binds the active UI locale through BDS.Desktop.UILocale.
In practice, this is the language of the app itself.
2. Render Localization
Rendered site output uses a separate locale flow. Archive labels, pagination text, template-facing render strings, and generated site language handling use the render Gettext domain and the project's main_language and blog_languages settings.
In practice, this is the language of the blog output, not necessarily the UI.
3. Content Translation
Posts and media have translation-aware workflows. Post translations and media metadata translations are modeled explicitly, and generation/preview/publishing use the project's configured languages when building output.
Relevant translation resources live under:
- priv/gettext/ for Gettext catalogs
- priv/i18n/ for additional locale data used by the app
If you touch i18n-sensitive behavior, check whether the change belongs to UI locale, render locale, or content translation. They are related, but they are not interchangeable.
Frontend And Assets
Frontend source now follows the Phoenix asset layout:
- assets/css/ for Tailwind-based CSS modules
- assets/js/ for LiveView hooks, bridges, Monaco integration, and UI helpers
- priv/static/assets/ for generated outputs
The rule of thumb is simple:
- common layout, spacing, state, and typography belong in HEEx and small shared UI primitives
- authored CSS stays for tokens and desktop-specific selectors
- JavaScript stays focused on LiveView hooks, editor integration, drag/drop, and browser APIs
Repository Map
- mix.exs: Mix project definition, aliases, releases, and dependencies
- config/: runtime, dev, test, and asset configuration
- lib/bds/: core application modules
- lib/bds/desktop/: desktop endpoint, shell, menus, controllers, and window integration
- assets/: Tailwind and esbuild source
- priv/repo/: Ecto migrations and snapshots
- priv/gettext/: UI and render translation catalogs
- specs/: Allium behavior specs
- DOCUMENTATION.md: end-user guide
- API.md: generated scripting API reference
macOS Development Setup
If you are setting up a new macOS machine, install the toolchain first.
1. Install Xcode Command Line Tools
xcode-select --install
2. Install Homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
3. Install Erlang, Elixir, and SQLite
brew update
brew install erlang elixir sqlite
4. Fetch Dependencies And Set Up The App
cd /Users/gb/Projects/bDS2
mix setup
mix assets.setup
Development Workflow
Useful commands:
mix compile --warnings-as-errors
mix test
mix dialyzer
mix assets.build
Headless Server Mode And Terminal UI
Since issue #26 the app has three boot modes, selected with the BDS_MODE
environment variable (any release or dev boot; default is the desktop app):
desktop— the native desktop app, unchanged.server— headless: no window, loopback-only HTTP endpoint, CLI sync watcher, and an SSH daemon serving the terminal UI. Works on macOS, Windows, and Linux (no display needed).tui— the headless server plus a TUI attached to the launching terminal.
Build and run a headless server
MIX_ENV=prod mix release bds --overwrite
BDS_MODE=server _build/prod/rel/bds/bin/bds start
The SSH daemon listens on port 2222 (BDS_SSH_PORT or
config :bds, :server, ssh_port: to change). Authentication is public-key
only: on first boot the server generates a host key and an empty
authorized_keys (mode 600) in ssh/ next to the database (default
~/Library/Application Support/BDS2/ssh/, or under BDS_DATABASE_PATH).
Add your public key there, then connect from any terminal:
ssh -p 2222 <server-host> # opens the TUI
Each SSH connection gets its own TUI session on the server; only terminal
cells cross the wire. All clients stay synchronized through the domain
event bus (BDS.Events) — a post created by a pipeline or another client
appears in every open shell.
TUI keys
↑/↓ or j/k navigate · enter open · n new post · 1-5 switch view
(posts/media/templates/scripts/tags) · : command prompt (metadata
diff, validate site, force render, rebuilds, …; :? lists all) ·
metadata-diff/site-validation reports open as panels — enter applies
the whole report (repair from files / incremental apply), esc cancels ·
r refresh · in the editor:
ctrl+s save, ctrl+p publish, ctrl+e word-wrapped preview,
ctrl+t title, ctrl+l language, ctrl+g AI suggestions
(airplane-mode gated), esc back · ctrl+q quit.
Images preview inline via the terminal image protocol. The UI language is
the server-side setting shared by every client.
Remote GUI
The HTTP endpoint stays bound to 127.0.0.1 and is never exposed; GUI
clients tunnel through the same SSH daemon and keys:
ssh -p 2222 -L 4010:127.0.0.1:4010 -N <server-host> &
open http://127.0.0.1:4010
Or use the desktop app directly: File → Connect to Server… takes
user@host[:port], connects with the client key from the same local
ssh/ directory (public-key auth, trust-on-first-use known_hosts),
tunnels to the server, and points the window at it. File → Disconnect
from Server returns to the local workspace.
Monaco Editor
Monaco is bundled via esbuild using its ESM entry point (monaco-editor/esm/vs/editor/editor.main.js). The
main bundle is built as part of mix assets.build and lives at priv/static/assets/monaco.js and
monaco.css; ESM worker bundles live under priv/static/assets/monaco/.
mix monaco.version # show current monaco-editor version
mix monaco.update 0.55.1 # update to a specific version and rebuild the bundle
The update task runs npm install monaco-editor@<version>, cleans the old bundle, and rebuilds via esbuild.
See https://www.npmjs.com/package/monaco-editor for available versions.
Notes for developers:
- Specs in specs/ define the intended product behavior.
- DOCUMENTATION.md is for end users, not implementation details.
- API.md is generated from the live scripting capability map and should stay in sync with runtime changes.
- When changing persistence or localization behavior, check both the database side and the filesystem/render side before assuming the change is complete.
Packaging The macOS App
mix bds.bundle.macos produces a self-contained, ad-hoc-signed BDS2.app. It
bundles the :bds release (ERTS included) plus the wxWidgets dylibs relocated
via @loader_path, so it runs on a clean Mac with nothing installed. The bundle
registers the bds2:// URL scheme (for blogmark deep links) and ships the red-pen
AppIcon. Output: dist/macos/BDS2.app. (arm64 / Apple Silicon only.)
1. Build the release, then the bundle
MIX_ENV=prod mix release bds --overwrite
mix bds.bundle.macos --app-release _build/prod/rel/bds
The icon .icns is generated on first run from
priv/desktop/macos/icon-source.svg
and committed under priv/desktop/macos/. Pass --regen-icon to rebuild it.
2. Register the bds2:// scheme (optional)
--register tells LaunchServices about the scheme without moving the app to
/Applications — useful for testing blogmark deep links locally:
mix bds.bundle.macos --app-release _build/prod/rel/bds --register
Run / smoke-test
open dist/macos/BDS2.app
open "bds2://new-post?title=Hello&url=https://example.com" # opens a draft
Useful flags
--app-release PATH— release dir to wrap (default_build/<env>/rel/bds).--output DIR— output directory (defaultdist/macos).--version V— version string forInfo.plist(default frommix.exs).--regen-icon— regenerateAppIcon.icnsfrom the source SVG.--register— register thebds2://scheme via LaunchServices.--no-codesign— skip the ad-hoc codesign step.
Verifying the bundle
plutil -lint dist/macos/BDS2.app/Contents/Info.plist
codesign --verify --deep --strict dist/macos/BDS2.app
# no /opt/homebrew or /usr/local references should appear:
otool -L dist/macos/BDS2.app/Contents/Resources/rel/lib/wx-*/priv/wxe_driver.so