-- 007: first-party auth (email + password) and membership invitations. -- Passwords live in their own table (never in users, never in audit diffs); -- hashes are scrypt with a per-user salt, produced by apps/api/src/auth/password.ts. CREATE TABLE user_passwords ( user_id uuid PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, password_hash text NOT NULL, updated_at timestamptz NOT NULL DEFAULT now() ); -- Invited members exist as memberships before they first sign in. ALTER TABLE memberships ADD COLUMN status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'invited')); GRANT SELECT, INSERT, UPDATE, DELETE ON user_passwords TO sp_app; -- Login happens before any tenant context exists, and memberships is FORCE -- RLS. This narrow SECURITY DEFINER lookup returns only the caller-supplied -- user's own memberships so login can pick a workspace without weakening RLS. CREATE FUNCTION auth_user_memberships(p_user_id uuid) RETURNS TABLE(workspace_id uuid, role text, status text, created_at timestamptz) LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$ SELECT m.workspace_id, m.role, m.status, m.created_at FROM memberships m WHERE m.user_id = p_user_id ORDER BY m.created_at ASC $$; REVOKE ALL ON FUNCTION auth_user_memberships(uuid) FROM PUBLIC; GRANT EXECUTE ON FUNCTION auth_user_memberships(uuid) TO sp_app;