Files
Ilker ManapandClaude Opus 5.5 098886e0e4 Let a person belong to many places, or to none
A person was in exactly one project, chosen at approval, and could not
use the gateway before that. People belong to several teams and to
none, so:

- memberships: a person is a member of any number of organizations,
  teams and projects, or of none; a member of a team or organization
  may use every project in it. users join/leave, and the people page
  adds and takes out from either side. Approval no longer needs a
  project.
- personal keys: a key may have an owner and no project. /profile asks
  what a new key spends against; keys create --for does the same.
  auth.personal sets default limits for personal use or turns it off.
- limits for people: --scope user holds a person across every key,
  project and the chat page; --per-member on an org, team or project
  gives each member their own share there. Resolution folds the tree,
  each per-member limit for the key's owner, and the owner's own, each
  in its own pool; the quotas page shows and sets both.
- the chat page asks what to spend against and checks it per message.
- usage records who spent it, so a person's budget survives a restart,
  personal use and chat included; restore no longer drops rows with no
  project.

Leaving a project revokes the keys made there unless another membership
still reaches it. A personal key keeps no threads or notes, which are
per project, and is refused rather than dropped into the anonymous pool.

Migration 0010 turns each person's project into a membership, adds
usage.user_id and backfills it, and makes api_keys.project_id nullable.
SQLite cannot drop NOT NULL, so the table is rebuilt; that needs foreign
keys off, which only works outside a transaction, so migrations marked
for it run on one connection with foreign_key_check before commit.
Tested on a populated SQLite store with claims, and on a copy of the
cluster's PostgreSQL store: 5 people became members, 17 keys and 3
claims intact, 145 of 577 usage rows and all 69 chat rows attributed.
The PostgreSQL store tests, including new membership cases, pass on 16.

users move is gone; join and leave replace it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 19:03:46 +02:00

36 lines
1.7 KiB
SQL

-- A person could be in exactly one project, chosen once at approval, and could
-- not use the gateway at all before that. People belong to several teams and
-- to none, so membership becomes its own table and a key need not have a
-- project: a personal key is owned by a person and bounded by that person's
-- own limits.
-- Membership at any level of the tree; a member of a team may use every
-- project in it. Role is 'member' today; 'manager' is reserved for delegated
-- administration of the scope.
CREATE TABLE memberships (
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
scope_type TEXT NOT NULL,
scope_id TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'member',
added_at BIGINT NOT NULL,
added_by TEXT,
PRIMARY KEY (user_id, scope_type, scope_id)
);
CREATE INDEX memberships_by_scope ON memberships(scope_type, scope_id);
-- Everybody keeps the project they had.
INSERT INTO memberships (user_id, scope_type, scope_id, role, added_at, added_by)
SELECT id, 'project', project_id, 'member', COALESCE(approved_at, created_at), approved_by
FROM users WHERE project_id IS NOT NULL;
-- A personal key has an owner and no project.
ALTER TABLE api_keys ALTER COLUMN project_id DROP NOT NULL;
-- Who spent it, recorded with the spend. Deriving it from the key at read time
-- counted a key's past against whoever owns it now, and could not attribute
-- the chat page's requests, which have no key row at all.
ALTER TABLE usage ADD COLUMN user_id TEXT NOT NULL DEFAULT '';
UPDATE usage SET user_id = COALESCE((SELECT k.user_id FROM api_keys k WHERE k.id = usage.key_id), '');
UPDATE usage SET user_id = substr(key_id, 9) WHERE key_id LIKE 'session:%';
CREATE INDEX usage_by_user_ts ON usage(user_id, ts);