feat(db): add user_settings table for account-wide settings (Chat)

Merged
HA
2d ago
+17315-0

Summary

  • Add a user_settings table: account-wide settings that are the same on every device and readable by the server. Its first column is experimental_chat (open new ACP agent sessions as chats), off by default.
  • Migration only. The router and client changes are in the stacked PR (linked below).

Why / Context

Chat is moving off the acp-chat PostHog flag to a setting people turn on themselves. It has to be account-level: a device-local switch lets desktop and mobile disagree, and the server (automations, cloud workspace starts) can't see it. The first version added a column to auth.users. Per review, settings stay off that table, which every authenticated request reads, so this PR now adds its own table instead.

Migration

CREATE TABLE "user_settings" (
	"user_id" uuid PRIMARY KEY NOT NULL,
	"experimental_chat" boolean DEFAULT false NOT NULL,
	"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
ALTER TABLE "user_settings" ADD CONSTRAINT "user_settings_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;

Generated by drizzle-kit generate --name=user_settings; not hand-edited. Same shape as channel_notification_prefs (0166): user_id primary key, typed columns with defaults, no row means the defaults. Chat is therefore off for every account with no backfill.

The PR has two commits (column, then replaced by the table). The net diff is only the table. Squash on merge.

Safety checklist (.agents/skills/db-migrations)

  • Production sizes: user_settings is new and empty. auth.users (27 MB, ~69k rows) is only referenced by the foreign key
  • No column type change
  • Lock order: child first. The new table is created, then the foreign key takes a brief SHARE ROW EXCLUSIVE on auth.users. That lock blocks writes, not reads, and is held only while the constraint is validated against the empty table
  • No long ACCESS EXCLUSIVE on a hot table. auth.users is not altered
  • Every statement finishes well inside 60 s
  • Rehearsal under load: not run. The only lock on a hot table is the foreign key's brief write lock on auth.users, validated against an empty table. Applying migrations to the workspace's Neon dev branch was also blocked by the sandbox's permission check
  • No reads move to data that isn't there (missing row means defaults)
  • No tRPC input/output change in this PR

Rollout / Rollback

  • Merge first, and let deploy-database apply it. The stacked PR reads the table and must merge after this deploys.
  • Rollback: the table is unused until the stacked PR ships; dropping it needs a follow-up migration.