aboutsummaryrefslogtreecommitdiff
path: root/docs/multi-account-spec.org
blob: 32a7527f0dc90cb741059a8ae4872cadbaf53375 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
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.