01
Multi-platform delivery
Versioned builds for Apple Silicon macOS, Windows, and a signed universal Android APK, produced by a repeatable release workflow.
Open release v0.24.1 →Jodd case study · BBMedia
Founder-led · hands-on · AI-directed
Jodd began as Kaiwan Hongladaromp's way to get close to product building again after years in senior technology leadership. A real cross-device notes problem became a public test of whether experience, hands-on engineering, and carefully directed AI could ship together.
01 · Starting point
Kaiwan's career spans more than three decades across internet infrastructure, telecommunications, payments, and regulated technology. After years working at leadership and architecture altitude—most recently as chief technology officer of a licensed payment provider—he wanted to prove something to himself before claiming it to anyone else: that he could return to day-to-day product building and use modern AI tools with judgment rather than novelty.
The problem was personal and concrete. His notes lived comfortably on a Mac, but more work was moving across Windows and Android. A small note-sharing experiment grew into a technically demanding product playground: local-first state, Gmail-backed Apple Notes round-trip, multi-device UX, OAuth, sync conflict handling, and safe agent access.
The goal was not to invent a startup story first. It was to build enough truth that the story could be inspected afterwards.
02 · What shipped
Jodd is still a Developer Preview. Its value as a showcase comes from making the working parts and the unfinished parts equally visible.
01
Versioned builds for Apple Silicon macOS, Windows, and a signed universal Android APK, produced by a repeatable release workflow.
Open release v0.24.1 →02
SQLite is the immediate source of truth for the interface; background workers reconcile Gmail or Local Folder storage without putting backend latency in normal editing.
Read the architecture →03
An optional MCP server can search notes and perform deny-by-default, folder-allowlisted writes. Stale or ambiguous task edits are refused instead of guessed.
Inspect the MCP boundary →04
Permissions, local caching, desktop signing limitations, feature gaps, release provenance, and dependency alerts are stated and checked rather than hidden behind launch copy.
Read what changed →03 · How it is built
Every edit is written to a local database and reflected on screen in the same synchronous step. A background worker reconciles with Gmail, Microsoft Graph, or a local folder afterwards. The user is never on the far side of that boundary.
The write path. Everything above the dashed line completes before the keystroke returns; everything below it happens on a five-second tick the user never waits for.
A keystroke updates the Svelte store and calls
apply_local_edit, which writes the row and sets
sync_state = 'dirty' in one SQLite transaction. Control
returns immediately. On the next tick the sync worker calls
list_dirty, pushes through the account's vertical, then
mark_pushed returns the row to clean.
An AppState.pushing set holds
(account_id, uuid) pairs for in-flight pushes, so the
next poll does not mistake Jodd's own write for a remote-side change
and manufacture a conflict against itself.
The rule this enforces, stated in the project's own contributor documentation: any normal navigation or editing command that blocks on the remote is a bug, and any frontend state mutation that happens after an awaited IPC call is a bug.
Gmail, Microsoft Graph, and a plain local folder differ in authentication, in how a note is identified, and in what saving one even means. They share everything above the transport seam.
The vertical abstraction. Three backends resolve to one
Box<dyn Vertical>, and no single line divides them.
All three save differently; identity pairs Gmail with a local folder,
both keyed on Apple's X-Universally-Unique-Identifier,
which Exchange lacks; authentication pairs Gmail with Exchange, both
OAuth 2.0 with PKCE, against a local folder with none.
vertical_for(account) dispatches on the account's
backend kind. The sync worker, the conflict reconciler, and every
command are written once against the trait rather than once per
backend.
The differences are real, not cosmetic. Gmail has no replace
operation, so a save is insert-new plus trash-old and the cached
message id must be repaired afterwards. Microsoft Graph
PATCHes in place, so a folder change needs a separate
explicit move call. A local folder has no account to authenticate —
readiness is "the directory exists."
Identity splits two against one. Gmail notes and local-folder notes
are both keyed on Apple's
X-Universally-Unique-Identifier header — a local folder
stores each note as an .eml file carrying that same
header — while an Exchange note carries none of Apple's headers at
all and is keyed on internetMessageId. The RFC 822
builder — build_note_mime — splits the same way: shared
by those same two, deliberately unused by the third. The module it
lives in does not split so cleanly: the Exchange vertical still
borrows one format-neutral helper from it,
format_apple_uuid, to mint a note id.
Notice that the two splits cut crosswise. Identity pairs Gmail with the local folder against Exchange; authentication pairs Gmail with Exchange against the local folder. No ordering of these three backends puts the odd one out in the same place twice.
04 · How the data is handled
When the same note is edited in two places, neither version is discarded. A conflict copy is created with the local content and the primary row converges to the remote. The user resolves it by editing; the software never picks a winner on their behalf.
Note sync states and the keep-both branch. A conflict produces a
second row with a fresh identity rather than overwriting either side —
and it does so without passing through a conflict state:
pull_needed and conflict are declared in the
SyncState enum and reserved, but no path writes either
one today.
reconcile_one takes the keep-both branch when a note is
locally dirty and its remote version token
changed since the last sync. It creates a copy under a fresh UUID,
titled with a (conflict from {Device} {Date}) suffix,
preserving the local content, and lets the primary row converge to
the remote version. The copy is inserted dirty, so the
worker pushes it too — both versions end up on the account, and the
user resolves by editing one and deleting the other.
"Version token" rather than "message id" is deliberate. Each
backend supplies its own opaque change token — Gmail's is the
message id, Microsoft's is lastModifiedDateTime, a
local folder's is the Date header — and the comparison
is written against the token, not against any one backend's idea of
identity.
Two of the five declared note states are, as of this writing,
unreached. SyncState declares
clean · dirty · pull_needed · conflict · deleted_pending,
but nothing in the codebase writes pull_needed, and
every route into conflict is closed behind it. The
three conditional writers all take the same
pull_needed → conflict branch — the edge inside the
dashed frame above — and wait on a state that never arrives: two
sit in the
shared body behind apply_local_edit, and the third in
move_notes_batch, which its own doc comment describes
as transitioning through the same state machine. The one
unconditional writer is a helper with no callers.
So the keep-both branch runs entirely between dirty and
clean. Both states are left declared rather than
deleted because the transitions out of them are already written,
though written is as far as it goes: no test writes either one.
The diagram draws them dashed and apart rather than
pretending they are live.
Folders carry a smaller state set —
clean · dirty_new · dirty_renamed · deleted_pending —
all four of which are reached. Their hierarchy is auto-completed on
both the locally-initiated and the pulled-from-backend path, so a
backend that permits a leaf label without its parent never produces
an orphaned row.
The local cache is encrypted at rest with AES-256 via SQLCipher, and the key lives in the operating system's own credential store — the same place the OAuth refresh token already lives.
Trust boundaries. There is no BBMedia server in this diagram, and its absence is the design — notes sync between the user's device and the user's own Gmail or Microsoft account, and nowhere else. The one BBMedia-hosted thing in the product touches no note: a static page that bounces the OAuth authorization code back into the app on Android, where the browser will not hand the callback over directly.
The cipher key is a 256-bit random value used with SQLCipher's raw-key syntax. It is deliberately not derived from a passphrase: the key is machine-generated and never human-typed, so there is no guessable input for a key-derivation function to slow down.
A failed keychain read is never treated as "no key yet."
load_key_hex returns Ok(None) only when the
entry genuinely does not exist; a locked credential store returns an
error. Collapsing the two would let a fresh key be written over a
good one and leave the already-encrypted database permanently
undecryptable.
A wrong key is caught at open rather than later.
PRAGMA key never errors on a wrong key — only the first
real read does — so open_encrypted forces a canary
query before returning.
The exception, stated plainly: a Local Folder vault
is not encrypted, on purpose. Encryption covers the cache of a
remote source of truth — one file, jodd.sqlite3. A
directly-readable folder of .eml files on a disk the
user chose is the entire point of that backend, and encrypting it
would remove the property it exists to provide.
The other exception, also stated plainly: Jodd reaches a BBMedia-hosted URL on its own initiative exactly once, and only during Android sign-in. Google will not hand an OAuth callback to an app mid-navigation, so the redirect lands on a static page that re-issues the authorization code as an Android intent. It carries no note content and runs no server-side logic. Two other BBMedia URLs are in the source — the Privacy and Terms buttons in the About panel — but those hand the address to the system browser on an explicit click, so the app itself never requests them. There is no telemetry, no analytics, and no crash reporter anywhere in the codebase, so the only hosts the sync path ever contacts are Google's and Microsoft's own API endpoints. Outside the sync path, the optional LLM features reach whichever provider the user configures — a decision that is entirely theirs, and unset by default.
05 · AI operating model
Jodd was developed with AI copilots and task-specific agents, but the operating system around them matters more than the model names.
The same loop, with what each step leaves behind. Two of the three gates are human, and rejected work returns to Delegate rather than moving forward. The third is machine-run — one of those three CI workflows is the Android encryption proof, which executes on a real emulator — and it stands between the workflows and the tagged releases because what it blocks is publication, not merge: a failure there leaves every binary built and nothing published. Figures measured 2026-08-16 — how these numbers were measured.
A four-step model is only worth describing if it changes what ships. This is one decision it produced, end to end. The part that settles it is not the attempt that failed — it is the control underneath, which is what separates the platform will not do this from we did not manage it.
POST /me/mailFolders and
POST /me/mailFolders/{id}/childFolders — and then the
one route those two leave open: create at mailbox root
with the class requested, then
POST /me/mailFolders/{id}/move it under Notes. All
three against a live account. It is the third that turns two
failures into a closed set.
500 ErrorObjectTypeChanged. Not sync lag: the folders
were still absent from Notes.app 21 hours later and after a forced
resync, and reading them back showed the class had silently come out
as IPF.Note even where IPF.StickyNote was
requested at creation. No sequence of Graph calls produces a folder
Apple will display.
false, permanently, in
Capabilities::for_backend. The folder-writing code
exists and is unit-tested; the capability gate keeps it switched off
and the interface stops offering it. The evidence sits in the doc
comment beside the flag.
A second decision — four constraints in the Android OAuth redirect chain, three of which are invisible from, or contradicted by, the vendor's own documentation — is written up in Engineering practice.
It does not claim that AI autonomously founded a company, that every generated first draft was correct, or that Jodd is already a consumer-ready business. It demonstrates a practical way for one experienced person to direct more parallel capability while keeping decisions, review, and responsibility human.
06 · Open learning trail
The public repository keeps a curated trail from problem framing to implementation. Internal handoffs and machine-specific execution notes are removed; the material that helps another builder understand the decisions remains open.
Work with BBMedia
A focused engagement for a founder or team with a promising idea, a half-built internal tool, or a technical product decision that has stayed abstract for too long. The aim is not more presentation material. It is enough working evidence to decide what to build next.
Start a scoped conversationScope, duration, and commercial terms are agreed before work begins. A prototype sprint does not replace independent security, legal, compliance, or production-readiness review where those are required.