Files
margin/simplify/guidelines/distribution.md
T
2026-10-03 22:48:49 +05:30

6.4 KiB

Distribution

Decided 2026-08-30 for Margin, Margin Calendar and Margin Docs. Margin Mail follows the same shape.

Channels

All the apps are free. No licence gate, no in-app purchase, so Apple takes no cut and the App Store anti-steering rules do not apply.

The Mac App Store is the primary macOS channel: sandboxed, no self-updater, a separate build track. Direct download from margin.73ai.org stays the unrestricted build carrying the Tauri updater. macOS also ships through the user's own Homebrew tap, priyanshujain/homebrew-margin, rather than upstream homebrew-cask, which the repos do not clear the notability bar for.

Linux ships through a Nix flake. Do not propose the AUR again: the AUR job and PKGBUILD were removed on 2026-09-03 because the user has no AUR account and signups are restricted. The Nix package is a binary repackage of the released deb, for the same reason the AUR one was, which is that the Google OAuth client is embedded at compile time from a file that is not in the repo, so a from-source build by a stranger produces an app that cannot connect. The wrapper sets a *_PACKAGED_BY=nix variable so the in-app updater announces new versions without trying to install over the store.

Nix is not installed on the user's Mac. Test a flake through the amd64 nixos/nix Docker image with filter-syscalls = false, since seccomp fails under emulation, and a named volume on /nix.

No migration bridge was built for the library moving into the App Store sandbox container. There are effectively no existing users and a first-run import is not worth building yet.

Licensing

Margin is FSL-1.1-MIT: free for anything except a competing product, becoming MIT two years after each release. Chosen because the user wants open code that nobody else monetises, which is not open source by the OSI definition. AGPL was ruled out because it is incompatible with the Mac App Store. Margin Mail is FSL-1.1-MIT too.

Margin Calendar and Margin Docs are still MIT and have not been relicensed. That is an open question, and consolidating shared code into one package forces it: shared code cannot be under two licences.

The updater pubkey lives in an overlay

plugins.updater.pubkey and bundle.createUpdaterArtifacts live in src-tauri/tauri.release.conf.json, a release-only overlay merged with --config in the GitHub Actions release workflow. They are deliberately kept out of the committed tauri.conf.json.

The reason is a Tauri bug (tauri-apps/tauri#14581): the mere presence of plugins.updater.pubkey in tauri.conf.json makes tauri build demand a signing key, which breaks the local key-free build. The overlay scopes signing and updater artifacts to CI. Only CI-built signed releases need to self-update anyway.

The release workflow is a manual workflow_dispatch with prepare, a build matrix and publish. The matrix should be max-parallel: 1, because tauri-action merges latest.json read-modify-write across platforms and parallel jobs race.

It is not, in any app. Checked on 2026-09-06: margin/.github/workflows/release.yml:91, margin-caledar/.../release.yml:85 and margin-mail/.../release.yml:85 set fail-fast: false and nothing else, and Margin Docs has no matrix at all. The constraint was recorded once and then lost, which is exactly the failure this consolidation is for. The shared build job in ../release.md sets it.

Signing

Signing keys and CSRs live in ~/.margin-signing: Developer ID Application, Apple Distribution, the installer certificate and the App Store Connect key, each with a .pass file. They were generated with openssl rather than Keychain Access so CI .p12 files can be rebuilt without a GUI. The Developer ID is in the login keychain and codesign uses it without prompting.

Never print the contents of anything in that directory.

Google Cloud

One Cloud project, margin-500217, numeric id 205537985128, owned by [email protected] and not by the Google account signed into the apps. Margin Calendar's desktop google-credentials.json is a copy of Margin's, so they share one OAuth desktop client, which means they share the consent screen and the enabled API list.

Enabling an API is per project, not per client. On 2026-08-10 the calendar scope was granted correctly and every calendarList call still returned 403 SERVICE_DISABLED, because the Calendar API had never been enabled on that project. A gcloud services enable run as the gmail account was denied, because that account does not own the project. If a Google resource comes back empty while auth succeeds, check the API is enabled before suspecting sync, and run the enable as [email protected].

Phones share the desktop client deliberately. A Desktop client may redirect to loopback on any port without registering it, and Google's token endpoint checks the client id, secret and redirect rather than the calling OS. Verified on 2026-08-12 on both an iOS simulator and an Android emulator. This works because the protocol does not check, not because Google blesses it; the fallback if they ever enforce it is a per-platform client, which the code already supports.

One thing still argues for a real iOS client: it switches iOS to ASWebAuthenticationSession, which shares Safari's session, so the user is not asked to sign in again. Android needs nothing, because Chrome Custom Tabs already share Chrome's cookies, which was measured rather than assumed. The iOS session sharing could not be confirmed on the simulator and needs a real device.

The client secret is not confidential for an installed app. Embed it, through the same release overlay pattern as the updater pubkey, so local builds stay clean.

Refresh tokens are not in any keychain. They are XChaCha20-Poly1305 sealed in a file under the app data directory, so the old security find-generic-password check no longer applies anywhere.

Never touch cloud infrastructure unasked

Reading is fine: list, describe, get, dry runs. Changing is not. Do not create, delete, rename or reconfigure a project, bucket, database, service, key, credential, IAM binding or DNS record, do not enable or disable an API, and do not attach billing without being asked for that exact thing on that exact resource. Permission does not carry forward to the next request.

If something turns out to be blocked, say it is blocked and why. Do not route around it. A workaround that provisions new infrastructure is a much bigger decision than the one that was made.