-- 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);