aboutsummaryrefslogtreecommitdiff
path: root/docs/multi-account-spec.org
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-24 18:46:43 -0500
committerCraig Jennings <c@cjennings.net>2026-05-24 18:46:43 -0500
commit99908a72283e16f060239cff176a3b971e509d3c (patch)
treec24d78248caa2ef9d4eb8b34307f9bcd9b8f0143 /docs/multi-account-spec.org
parent0683e6aeb0d4a3855dd841611ccfbb6221cfb506 (diff)
downloadpearl-99908a72283e16f060239cff176a3b971e509d3c.tar.gz
pearl-99908a72283e16f060239cff176a3b971e509d3c.zip
docs: add the ticket-save-model and multi-account specs
Two design specs for upcoming work. ticket-save-model-spec covers the unified "save the ticket" model — diff title/description/comments against their provenance hashes and push only what changed, with a sequential conflict-aware engine and an opt-in keymap — and went through two review rounds (Codex). multi-account-spec covers switching between work and personal Linear workspaces over the existing globals, with auth-source credentials and per-account cache isolation; it's still pre-review draft.
Diffstat (limited to 'docs/multi-account-spec.org')
-rw-r--r--docs/multi-account-spec.org122
1 files changed, 122 insertions, 0 deletions
diff --git a/docs/multi-account-spec.org b/docs/multi-account-spec.org
new file mode 100644
index 0000000..32a7527
--- /dev/null
+++ b/docs/multi-account-spec.org
@@ -0,0 +1,122 @@
+#+TITLE: pearl — Multi-Account Support Spec
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-05-24
+#+STARTUP: showall
+
+* Status
+
+*DRAFT — design proposal; nothing in =pearl.el= has changed.* Open questions for Craig at the end.
+
+Lets one Emacs talk to more than one Linear workspace — a work account and a personal account — and switch between them without re-customizing. Raised by Craig: "I have a work account and a personal account. I would like to choose which account I'm working on easily and switch back and forth between the two fairly straightforwardly."
+
+* Problem
+
+Everything that identifies a workspace is a single global value today:
+
+- =pearl-api-key= — one key.
+- =pearl-graphql-url= — one endpoint (always the same for Linear, but conceptually per-account).
+- =pearl-org-file-path= — one active file.
+- =pearl-default-team-id= — one team.
+- the lookup caches (=pearl--cache-teams=, =-states=, =-team-collections=, =-views=, =-viewer=) — global, and *workspace-specific*: a personal account's teams and a work account's teams must never bleed together.
+
+To work the other account you'd re-customize the key (and team, and file) by hand and clear the cache. There's no notion of "which account am I on."
+
+* Current state
+
+- =pearl-api-key= is a plaintext defcustom, optionally loaded from =LINEAR_API_KEY= via =pearl-load-api-key-from-env=. =pearl--headers= reads it directly and errors when unset.
+- =pearl-org-file-path= defaults to =gtd/linear.org=; one file holds the active view.
+- The caches are module-level =defvar=s; =pearl-clear-cache= resets them all. Nothing scopes them to a workspace, so switching keys without clearing the cache would serve stale (wrong-account) teams/views/viewer.
+- =pearl--cache-viewer= caches "who am I" — inherently per-account.
+
+* Proposed design
+
+** The account model
+
+A new defcustom =pearl-accounts=: an alist of named accounts, each a plist of the per-workspace settings.
+
+#+begin_src elisp
+(setq pearl-accounts
+ '(("work" :api-key-source (auth-source "api.linear.app" "work")
+ :org-file "~/org/work-linear.org"
+ :default-team-id "TEAM_WORK")
+ ("personal" :api-key-source (auth-source "api.linear.app" "personal")
+ :org-file "~/org/personal-linear.org"
+ :default-team-id nil)))
+#+end_src
+
+Each entry carries what's currently global: the credential, the org file, the default team, and (rarely needed) the endpoint. =:api-key-source= is how the key is *found*, not the key itself (see Credentials below).
+
+=pearl-active-account= holds the current account name. =pearl-default-account= picks the one used at startup.
+
+** Switching accounts
+
+=pearl-switch-account= (interactive): =completing-read= over =pearl-accounts= names, then:
+
+1. Set =pearl-active-account=.
+2. Resolve and bind the active account's settings — the key, the org file, the default team — so the existing code keeps reading =pearl-api-key= / =pearl-org-file-path= / =pearl-default-team-id= unchanged (they become "the active account's values").
+3. *Clear the lookup caches* — they're workspace-specific; a switch must invalidate teams/states/collections/views/viewer so the next lookup refetches against the new workspace.
+4. Optionally surface the new account's org file.
+
+The cleanest implementation keeps the rest of the package oblivious: a single resolver sets the three global-looking variables from the active account, and everything downstream (=pearl--headers=, =pearl--update-org-from-issues=, =new-issue=) works as-is. The accounts layer is a front-end over the existing globals.
+
+** Credentials
+
+Storing two API keys in plaintext customize is the wrong default. *Proposed: resolve keys through =auth-source=* (so =~/.authinfo.gpg= holds them), keyed per account. =:api-key-source= names the host + user to look up. A literal =:api-key "lin_..."= form stays supported as an escape hatch (and for the env-var path), but the documented default is encrypted auth-source. This also fixes a latent issue with the single-key model — the key sits in customize today.
+
+** Cache isolation
+
+Two options (open question 1):
+
+- *Clear on switch* (simpler): the caches stay global =defvar=s; =switch-account= clears them. Correct as long as every switch goes through the command. Minimal change.
+- *Keyed by account* (stricter): the caches become per-account maps, so switching back to "work" reuses its warm cache instead of refetching. More code, better ergonomics for frequent flippers.
+
+Lean: clear-on-switch for v1 (it reuses =pearl-clear-cache=), keyed caches as a vNext nicety.
+
+** The active-file question
+
+Each account gets its own =:org-file= (proposed), so "work" issues and "personal" issues never share a buffer. Switching accounts switches which file is active. The alternative — one file with a per-account section — fights the active-file model (one view at a time) and risks cross-account =LINEAR-ID= collisions in the same buffer. Per-account files keep the existing single-active-view model intact, just parameterized by account.
+
+** What's account-scoped
+
+Everything that hits the API or writes the file: list/view/saved-query fetches, all the sync/save commands, field setters, comment commands, =new-issue=, the caches, the viewer identity (for comment-edit permission — "your own comments" is per-account). The account is resolved once at switch time into the globals the commands already read, so no command needs to know about accounts individually.
+
+* Proposed v1 decisions (this feature)
+
+1. =pearl-accounts= alist (name → plist of key-source / org-file / default-team / optional url); =pearl-active-account= + =pearl-default-account=.
+2. =pearl-switch-account= sets the active account, resolves its settings into the existing global variables, and clears the caches.
+3. Credentials resolve through =auth-source= by default; a literal key stays supported.
+4. One org file per account (=:org-file=); switching switches the active file.
+5. The accounts layer is a front-end over the current globals — downstream commands are unchanged.
+
+* Files touched
+
+- =pearl.el=: =pearl-accounts= / =pearl-active-account= / =pearl-default-account= defcustoms; =pearl-switch-account= + a =pearl--resolve-account= helper that binds the globals; an =auth-source= key resolver; a one-time migration of the legacy single-key config; a transient/keymap entry for switching.
+- =docs/=: this spec.
+- =README.org=: a multi-account setup section (auth-source entries, example =pearl-accounts=).
+
+* Test plan
+
+- =pearl--resolve-account=: a named account's plist sets the expected key / org-file / default-team; an unknown name errors.
+- =switch-account= clears all five caches (assert each is nil after).
+- auth-source resolver: a stubbed =auth-source-search= yields the key; a missing entry gives a clear error naming the account, not a generic "key not set".
+- back-compat: with =pearl-accounts= nil and the legacy =pearl-api-key= set, everything works exactly as before (single-account path).
+- viewer cache: switching accounts invalidates =pearl--cache-viewer= so comment-edit permission re-resolves against the new account.
+
+* Migration
+
+Back-compatible. With =pearl-accounts= unset, the package behaves exactly as today off the legacy =pearl-api-key= / =pearl-org-file-path= / =pearl-default-team-id=. First-run-with-accounts can offer to seed a single "default" account from the legacy values. The credential move to auth-source is opt-in: the literal-key form keeps working, so no one is forced to migrate secrets to adopt accounts.
+
+* Open questions for Craig
+
+1. *Cache strategy*: clear-on-switch (simple, refetches after every flip) or per-account keyed caches (warm on switch-back, more code)? How often do you actually flip — a few times a day, or constantly?
+2. *Credential default*: auth-source / =~/.authinfo.gpg= as the documented path, with literal-key as the escape hatch — good? Or do you keep keys somewhere else (a password manager, env vars per account)?
+3. *Active file*: one file per account (proposed) versus a single file you re-point — any reason you'd want both accounts' issues reachable without switching?
+4. *Switch surface*: a plain =M-x pearl-switch-account=, plus a mode-line indicator of the current account? The indicator matters most — pushing a work edit while you think you're on personal (or vice versa) is the failure mode worth guarding against.
+5. *Endpoint*: is =graphql-url= ever actually different per account (self-hosted / EU region), or is per-account =:url= over-engineering for two linear.app workspaces?
+
+* vNext / out of scope
+
+- Per-account keyed caches (if clear-on-switch proves annoying).
+- Showing both accounts at once (split buffers / frames) — explicitly out; the model is one active account at a time.
+- Per-account =pearl-saved-queries= (v1 shares the saved-query list across accounts; revisit if names collide or filters don't translate).
+- Auto-detecting the account from the active file when you visit it.