From 51ed688593b86b95bae3937cbf5e1c99e900e385 Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Sat, 3 Oct 2026 05:12:30 +0100 Subject: [PATCH 01/13] feat(db): add custom_reviews_granted to workspaces Admins need to give custom review instructions to specific free users without changing their plan. A new boolean column on workspaces, custom_reviews_granted (not null, default false), records that grant separately from billing, so a workspace's plan and its feature grant stay independent. --- src/db/schema.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/db/schema.ts b/src/db/schema.ts index fa826b9..f55ec4a 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -93,6 +93,10 @@ export const workspaces = pgTable('workspaces', { // the plan's run limit and resets to 0 with runsUsed when the month rolls // over, so a grant never recurs. bonusRuns: integer('bonus_runs').notNull().default(0), + // Operator grant: custom review instructions without a paid plan. + customReviewsGranted: boolean('custom_reviews_granted') + .notNull() + .default(false), // Bachs billing identifiers, set when the workspace subscribes. The customer // id links webhook events back to this workspace. bachsCustomerId: text('bachs_customer_id'), From 5784fe650a6aa7e7c13201c9b0ba05841270befb Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Sat, 3 Oct 2026 05:12:31 +0100 Subject: [PATCH 02/13] chore(db): add migration for the custom reviews grant Generated with drizzle-kit. Adds workspaces.custom_reviews_granted (boolean, not null, default false). The change is additive and every existing workspace starts without the grant. Already applied to the database. --- drizzle/0006_cultured_spirit.sql | 1 + 1 file changed, 1 insertion(+) create mode 100644 drizzle/0006_cultured_spirit.sql diff --git a/drizzle/0006_cultured_spirit.sql b/drizzle/0006_cultured_spirit.sql new file mode 100644 index 0000000..6752d1a --- /dev/null +++ b/drizzle/0006_cultured_spirit.sql @@ -0,0 +1 @@ +ALTER TABLE "workspaces" ADD COLUMN "custom_reviews_granted" boolean DEFAULT false NOT NULL; \ No newline at end of file From ce4dd02c6a26f469dbe9b0d9a193c077f28513a6 Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Sat, 3 Oct 2026 05:12:32 +0100 Subject: [PATCH 03/13] chore(db): add drizzle snapshot for migration 0006 Schema snapshot generated by drizzle-kit alongside the custom reviews grant migration, used to diff future migrations. --- drizzle/meta/0006_snapshot.json | 1134 +++++++++++++++++++++++++++++++ 1 file changed, 1134 insertions(+) create mode 100644 drizzle/meta/0006_snapshot.json diff --git a/drizzle/meta/0006_snapshot.json b/drizzle/meta/0006_snapshot.json new file mode 100644 index 0000000..07bc38c --- /dev/null +++ b/drizzle/meta/0006_snapshot.json @@ -0,0 +1,1134 @@ +{ + "id": "f66dae99-46bb-4bb7-abd5-ed6fd161a2eb", + "prevId": "cbec1e4a-31c9-46fd-a911-62555997db79", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.codebase_scans": { + "name": "codebase_scans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "repository_id": { + "name": "repository_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "scan_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "scanned_files": { + "name": "scanned_files", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "summary": { + "name": "summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cost_usd": { + "name": "cost_usd", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "codebase_scans_repository_id_repositories_id_fk": { + "name": "codebase_scans_repository_id_repositories_id_fk", + "tableFrom": "codebase_scans", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.findings": { + "name": "findings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_run_id": { + "name": "review_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "finding_severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "line_number": { + "name": "line_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "suggestion": { + "name": "suggestion", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "findings_review_run_id_review_runs_id_fk": { + "name": "findings_review_run_id_review_runs_id_fk", + "tableFrom": "findings", + "tableTo": "review_runs", + "columnsFrom": [ + "review_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_installations": { + "name": "github_installations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_login": { + "name": "account_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_type": { + "name": "account_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "installation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "github_installations_installation_id_idx": { + "name": "github_installations_installation_id_idx", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_installations_workspace_id_workspaces_id_fk": { + "name": "github_installations_workspace_id_workspaces_id_fk", + "tableFrom": "github_installations", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.page_views": { + "name": "page_views", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "referrer": { + "name": "referrer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "page_views_created_at_idx": { + "name": "page_views_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pull_requests": { + "name": "pull_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "repository_id": { + "name": "repository_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "github_pull_request_id": { + "name": "github_pull_request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_login": { + "name": "author_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_sha": { + "name": "base_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_open": { + "name": "is_open", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pull_requests_repository_id_repositories_id_fk": { + "name": "pull_requests_repository_id_repositories_id_fk", + "tableFrom": "pull_requests", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.repositories": { + "name": "repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "github_repo_id": { + "name": "github_repo_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "repository_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'watching'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "repositories_workspace_repo_idx": { + "name": "repositories_workspace_repo_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_repo_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "repositories_workspace_id_workspaces_id_fk": { + "name": "repositories_workspace_id_workspaces_id_fk", + "tableFrom": "repositories", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_runs": { + "name": "review_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "pull_request_id": { + "name": "pull_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "review_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "files_changed": { + "name": "files_changed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cost_usd": { + "name": "cost_usd", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_runs_pull_request_sha_idx": { + "name": "review_runs_pull_request_sha_idx", + "columns": [ + { + "expression": "pull_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "head_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_runs_pull_request_id_pull_requests_id_fk": { + "name": "review_runs_pull_request_id_pull_requests_id_fk", + "tableFrom": "review_runs", + "tableTo": "pull_requests", + "columnsFrom": [ + "pull_request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_hash_unique": { + "name": "sessions_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "token_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "github_id": { + "name": "github_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "onboarded_at": { + "name": "onboarded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_github_id_idx": { + "name": "users_github_id_idx", + "columns": [ + { + "expression": "github_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_settings": { + "name": "workspace_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "review_pull_requests": { + "name": "review_pull_requests", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "review_security": { + "name": "review_security", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "review_codebase_scans": { + "name": "review_codebase_scans", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "custom_instructions": { + "name": "custom_instructions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "min_severity": { + "name": "min_severity", + "type": "finding_severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'note'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_settings_workspace_id_workspaces_id_fk": { + "name": "workspace_settings_workspace_id_workspaces_id_fk", + "tableFrom": "workspace_settings", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_settings_workspace_id_unique": { + "name": "workspace_settings_workspace_id_unique", + "nullsNotDistinct": false, + "columns": [ + "workspace_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspaces": { + "name": "workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'free'" + }, + "runs_used": { + "name": "runs_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "runs_period_start": { + "name": "runs_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "bonus_runs": { + "name": "bonus_runs", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "custom_reviews_granted": { + "name": "custom_reviews_granted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bachs_customer_id": { + "name": "bachs_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bachs_subscription_id": { + "name": "bachs_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspaces_owner_id_users_id_fk": { + "name": "workspaces_owner_id_users_id_fk", + "tableFrom": "workspaces", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspaces_slug_unique": { + "name": "workspaces_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.finding_severity": { + "name": "finding_severity", + "schema": "public", + "values": [ + "critical", + "high", + "medium", + "low", + "note" + ] + }, + "public.installation_status": { + "name": "installation_status", + "schema": "public", + "values": [ + "active", + "suspended", + "deleted" + ] + }, + "public.repository_status": { + "name": "repository_status", + "schema": "public", + "values": [ + "watching", + "needs_setup", + "paused" + ] + }, + "public.review_run_status": { + "name": "review_run_status", + "schema": "public", + "values": [ + "queued", + "running", + "complete", + "failed" + ] + }, + "public.scan_status": { + "name": "scan_status", + "schema": "public", + "values": [ + "queued", + "running", + "complete", + "failed" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file From a7c193aaa033913f16ea96c160c0b530fd4f8911 Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Sat, 3 Oct 2026 05:12:33 +0100 Subject: [PATCH 04/13] chore(db): register migration 0006 in the drizzle journal Adds the 0006_cultured_spirit entry so drizzle-kit migrate applies the custom reviews grant migration. --- drizzle/meta/_journal.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 88aa1d2..31e5d1b 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -43,6 +43,13 @@ "when": 1790949594894, "tag": "0005_quiet_jetstream", "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1790999624041, + "tag": "0006_cultured_spirit", + "breakpoints": true } ] } \ No newline at end of file From 13234a5a78167f53c1e45336f691a5ea45d8b28a Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Sat, 3 Oct 2026 05:12:35 +0100 Subject: [PATCH 05/13] feat(billing): expose a single custom reviews entitlement WorkspaceBilling gains canCustomizeReviews, true when the workspace is on a paid plan or an admin has granted the feature. Every place that gates custom review instructions can now ask billing one question instead of checking the plan itself. Adds setCustomReviewsGranted(workspaceId, granted) to turn the grant on or off. --- src/server/billing.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/server/billing.ts b/src/server/billing.ts index 4016c0d..791af90 100644 --- a/src/server/billing.ts +++ b/src/server/billing.ts @@ -27,6 +27,8 @@ export type WorkspaceBilling = { runsUsed: number limit: number canRun: boolean + // Paid plans include custom review instructions; admins can grant them too. + canCustomizeReviews: boolean upgradeUrl: string } @@ -47,6 +49,7 @@ export async function getWorkspaceBilling( runsUsed: workspaces.runsUsed, runsPeriodStart: workspaces.runsPeriodStart, bonusRuns: workspaces.bonusRuns, + customReviewsGranted: workspaces.customReviewsGranted, }) .from(workspaces) .where(eq(workspaces.id, workspaceId)) @@ -65,10 +68,26 @@ export async function getWorkspaceBilling( runsUsed, limit, canRun: runsUsed < limit, + canCustomizeReviews: + plan !== 'free' || (rows[0]?.customReviewsGranted ?? false), upgradeUrl: getUpgradeUrl(), } } +// Operator grant: turn custom review instructions on or off for a workspace, +// independent of its plan. +export async function setCustomReviewsGranted( + workspaceId: string, + granted: boolean, +): Promise { + const { eq, db, workspaces } = await loadDb() + + await db + .update(workspaces) + .set({ customReviewsGranted: granted, updatedAt: new Date() }) + .where(eq(workspaces.id, workspaceId)) +} + // Increment the run counter when a workspace starts an agent run (review, // scan, or fix PR). Atomic, and rolls the monthly window: if the stored period // is from a previous month, the count resets to 1 and the period moves to now. From 5931928d934f8e593063232327a3494cd95eabc4 Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Sat, 3 Oct 2026 05:12:38 +0100 Subject: [PATCH 06/13] refactor(reviews): describe guidance gating as an entitlement effectiveReviewGuidance()'s second parameter was named `paid`. It's now `enabled`, since the guidance also applies to free workspaces with an admin grant. Behaviour is unchanged. --- src/lib/review-guidance.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lib/review-guidance.ts b/src/lib/review-guidance.ts index 075678d..dc583ee 100644 --- a/src/lib/review-guidance.ts +++ b/src/lib/review-guidance.ts @@ -60,12 +60,13 @@ export function customInstructionsPrompt( ].join('\n') } -// Saved guidance only applies while the workspace is on a paid plan. +// Saved guidance only applies while the workspace is entitled to it (a paid +// plan or an admin grant). export function effectiveReviewGuidance( saved: Partial | null | undefined, - paid: boolean, + enabled: boolean, ): ReviewGuidance { - if (!paid || !saved) return DEFAULT_REVIEW_GUIDANCE + if (!enabled || !saved) return DEFAULT_REVIEW_GUIDANCE return { customInstructions: normalizeCustomInstructions(saved.customInstructions), minSeverity: parseMinSeverity(saved.minSeverity), From 33fde77f4b49517ff541064c8fc0fcec1afd811f Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Sat, 3 Oct 2026 05:12:39 +0100 Subject: [PATCH 07/13] feat(settings): honour admin grants for custom review instructions The settings page shows the Custom review instructions card, and the server accepts saving it, whenever billing.canCustomizeReviews is true. That covers paid plans and free workspaces with an admin grant. Workspaces without either are still rejected on save. --- src/server/workspace.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/server/workspace.ts b/src/server/workspace.ts index d09747b..bb3ec31 100644 --- a/src/server/workspace.ts +++ b/src/server/workspace.ts @@ -1,7 +1,6 @@ import { createServerFn } from '@tanstack/react-start' import { loadDb } from '../db/load' -import { isPaidPlan } from '../lib/plans' import type { Plan } from '../lib/plans' import { DEFAULT_REVIEW_GUIDANCE, @@ -120,7 +119,7 @@ export const getWorkspaceSettings = createServerFn({ method: 'GET' }).handler( minSeverity: settingsRow.minSeverity, } : DEFAULT_REVIEW_GUIDANCE, - canCustomizeReviews: isPaidPlan(billing.plan), + canCustomizeReviews: billing.canCustomizeReviews, billing: { plan: billing.plan, runsUsed: billing.runsUsed, @@ -164,7 +163,7 @@ export const updateReviewPreferences = createServerFn({ method: 'POST' }) // Review guidance is a paid feature: free workspaces can't change it. if (guidance) { const billing = await getWorkspaceBilling(workspaceId) - if (!isPaidPlan(billing.plan)) { + if (!billing.canCustomizeReviews) { throw new Error('Custom review instructions need a paid plan.') } } From e5617631ef1afd85d4f05a2ea0276ce6839889ce Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Sat, 3 Oct 2026 05:12:42 +0100 Subject: [PATCH 08/13] feat(reviews): apply granted custom reviews on PR reviews Webhook-triggered reviews resolve the workspace's guidance with billing.canCustomizeReviews instead of checking for a paid plan, so admin-granted free workspaces get their instructions and minimum severity applied. --- src/server/github-webhook.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/server/github-webhook.ts b/src/server/github-webhook.ts index ea145dc..4449019 100644 --- a/src/server/github-webhook.ts +++ b/src/server/github-webhook.ts @@ -2,7 +2,6 @@ // the body is read as data), then pull-request events kick off a review run. import { loadDb } from '../db/load' -import { isPaidPlan } from '../lib/plans' import { effectiveReviewGuidance } from '../lib/review-guidance' import { runInBackground } from './background' import { getEnv } from './env' @@ -61,8 +60,7 @@ export async function verifyWebhookSignature( } export type WebhookResult = - | { handled: false; reason: string } - | { handled: true; reviewRunId: string } + { handled: false; reason: string } | { handled: true; reviewRunId: string } export async function handlePullRequestEvent( payload: PullRequestEvent, @@ -255,7 +253,7 @@ export async function handlePullRequestEvent( headSha: pr.head.sha, headRef: pr.head.ref, reviewSecurity: settings.reviewSecurity, - guidance: effectiveReviewGuidance(settings, isPaidPlan(billing.plan)), + guidance: effectiveReviewGuidance(settings, billing.canCustomizeReviews), // A fork's head branch doesn't exist in the base repo, so the fix-PR // pipeline cannot branch from it or open a PR against it. The review // itself works fine, so the run proceeds and only the fix PR is skipped. From 20764e37ae2e70fa6272804db17a83502973a4f7 Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Sat, 3 Oct 2026 05:12:43 +0100 Subject: [PATCH 09/13] feat(scans): apply granted custom reviews on codebase scans Scans resolve the workspace's guidance with billing.canCustomizeReviews, matching PR reviews, so admin-granted free workspaces get their settings applied to scans too. --- src/routes/api.scans.start.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/routes/api.scans.start.tsx b/src/routes/api.scans.start.tsx index 67c10d0..2d31e65 100644 --- a/src/routes/api.scans.start.tsx +++ b/src/routes/api.scans.start.tsx @@ -1,7 +1,6 @@ import { createFileRoute } from '@tanstack/react-router' import { loadDb } from '../db/load' -import { isPaidPlan } from '../lib/plans' import { effectiveReviewGuidance } from '../lib/review-guidance' import { getCurrentUserFromRequest } from '../server/github-auth' @@ -120,7 +119,7 @@ export const Route = createFileRoute('/api/scans/start')({ branch: repository.defaultBranch, guidance: effectiveReviewGuidance( settingsRows[0], - isPaidPlan(billing.plan), + billing.canCustomizeReviews, ), }), ) From bf96a2bfb740c00f6258b365c814069a3a1c1054 Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Sat, 3 Oct 2026 05:12:47 +0100 Subject: [PATCH 10/13] feat(admin): include and manage the custom reviews grant - AdminUserRow includes customReviewsGranted, read from workspaces.custom_reviews_granted, for the users table. - setCustomReviewsGrantAsAdmin(workspaceId, granted) grants or revokes the feature. As with the other admin actions, the API route re-checks the admin before calling it. --- src/server/admin.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/server/admin.ts b/src/server/admin.ts index 1a5de38..ff932cb 100644 --- a/src/server/admin.ts +++ b/src/server/admin.ts @@ -235,6 +235,7 @@ export type AdminUserRow = { plan: Plan | null runsUsed: number bonusRuns: number + customReviewsGranted: boolean repos: number reviews: number scans: number @@ -248,7 +249,7 @@ export const getAdminUsers = createServerFn({ method: 'GET' }).handler( const rows = await sqlClient` select u.id as user_id, u.username, u.name, u.email, u.avatar_url, u.created_at, - w.id as workspace_id, w.name as workspace_name, w.plan, w.runs_used, w.bonus_runs, + w.id as workspace_id, w.name as workspace_name, w.plan, w.runs_used, w.bonus_runs, w.custom_reviews_granted, (select count(*)::int from repositories rp where rp.workspace_id = w.id) as repos, (select count(*)::int from review_runs rr join pull_requests pr on pr.id = rr.pull_request_id @@ -273,6 +274,7 @@ export const getAdminUsers = createServerFn({ method: 'GET' }).handler( plan: r.workspace_id ? parsePlan(r.plan) : null, runsUsed: Number(r.runs_used ?? 0), bonusRuns: Number(r.bonus_runs ?? 0), + customReviewsGranted: r.custom_reviews_granted === true, repos: Number(r.repos ?? 0), reviews: Number(r.reviews ?? 0), scans: Number(r.scans ?? 0), @@ -294,6 +296,16 @@ export async function setWorkspacePlanAsAdmin( } } +// Admin-only: grant or revoke custom review instructions for a workspace. The +// API route re-checks the admin before calling this. +export async function setCustomReviewsGrantAsAdmin( + workspaceId: string, + granted: boolean, +): Promise { + const { setCustomReviewsGranted } = await import('./billing') + await setCustomReviewsGranted(workspaceId, granted) +} + // Admin-only: grant one-time bonus runs to a workspace's current window. The // API route re-checks the admin before calling this. export async function addWorkspaceRunsAsAdmin( From 263bac79d07f0061734343af2a069aac04ce4e1a Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Sat, 3 Oct 2026 05:12:48 +0100 Subject: [PATCH 11/13] feat(admin): add API route to grant or revoke custom reviews POST /api/admin/custom-reviews with { workspaceId, granted } turns custom review instructions on or off for a workspace. Like the other admin routes, it re-checks on the server that the caller is an admin (403 otherwise). It returns 400 unless workspaceId is a string and granted is a boolean. --- src/routes/api.admin.custom-reviews.tsx | 47 +++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/routes/api.admin.custom-reviews.tsx diff --git a/src/routes/api.admin.custom-reviews.tsx b/src/routes/api.admin.custom-reviews.tsx new file mode 100644 index 0000000..072170c --- /dev/null +++ b/src/routes/api.admin.custom-reviews.tsx @@ -0,0 +1,47 @@ +import { createFileRoute } from '@tanstack/react-router' + +import { isAdmin, setCustomReviewsGrantAsAdmin } from '../server/admin' +import { getCurrentUserFromRequest } from '../server/github-auth' + +// Admin-only: grant or revoke custom review instructions for a workspace. +// Re-checks the admin on the server; the UI gate is never trusted. +export const Route = createFileRoute('/api/admin/custom-reviews')({ + server: { + handlers: { + POST: async ({ request }) => { + const user = await getCurrentUserFromRequest(request) + if (!user || !(await isAdmin(user))) { + return json({ error: 'forbidden' }, 403) + } + + let body: { workspaceId?: unknown; granted?: unknown } = {} + try { + body = (await request.json()) as typeof body + } catch { + return json({ error: 'invalid body' }, 400) + } + + const workspaceId = + typeof body.workspaceId === 'string' ? body.workspaceId : '' + if (!workspaceId || typeof body.granted !== 'boolean') { + return json({ error: 'workspaceId and granted are required' }, 400) + } + + try { + await setCustomReviewsGrantAsAdmin(workspaceId, body.granted) + return json({ ok: true, granted: body.granted }, 200) + } catch (error) { + console.error('[admin] custom-reviews failed', error) + return json({ error: 'Unable to update custom reviews' }, 500) + } + }, + }, + }, +}) + +function json(body: unknown, status: number) { + return new Response(JSON.stringify(body), { + status, + headers: { 'content-type': 'application/json' }, + }) +} From 1156ebaa99b605896c8057669895c2d006df3b35 Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Sat, 3 Oct 2026 05:12:49 +0100 Subject: [PATCH 12/13] chore(routes): register /api/admin/custom-reviews Regenerated by the TanStack router generator to include the new admin API route. --- src/routeTree.gen.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 65ff1ec..6c06e31 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -29,6 +29,7 @@ import { Route as AuthSignInRouteImport } from './routes/auth.sign-in' import { Route as AuthSignUpRouteImport } from './routes/auth.sign-up' import { Route as UpgradeSuccessRouteImport } from './routes/upgrade.success' import { Route as ApiAdminAddRunsRouteImport } from './routes/api.admin.add-runs' +import { Route as ApiAdminCustomReviewsRouteImport } from './routes/api.admin.custom-reviews' import { Route as ApiAdminSetPlanRouteImport } from './routes/api.admin.set-plan' import { Route as ApiBillingCheckoutRouteImport } from './routes/api.billing.checkout' import { Route as ApiBillingWebhookRouteImport } from './routes/api.billing.webhook' @@ -146,6 +147,11 @@ const ApiAdminAddRunsRoute = ApiAdminAddRunsRouteImport.update({ path: '/api/admin/add-runs', getParentRoute: () => rootRouteImport, } as any) +const ApiAdminCustomReviewsRoute = ApiAdminCustomReviewsRouteImport.update({ + id: '/api/admin/custom-reviews', + path: '/api/admin/custom-reviews', + getParentRoute: () => rootRouteImport, +} as any) const ApiAdminSetPlanRoute = ApiAdminSetPlanRouteImport.update({ id: '/api/admin/set-plan', path: '/api/admin/set-plan', @@ -248,6 +254,7 @@ export interface FileRoutesByFullPath { '/upgrade/success': typeof UpgradeSuccessRoute '/app/': typeof AppIndexRoute '/api/admin/add-runs': typeof ApiAdminAddRunsRoute + '/api/admin/custom-reviews': typeof ApiAdminCustomReviewsRoute '/api/admin/set-plan': typeof ApiAdminSetPlanRoute '/api/billing/checkout': typeof ApiBillingCheckoutRoute '/api/billing/webhook': typeof ApiBillingWebhookRoute @@ -282,6 +289,7 @@ export interface FileRoutesByTo { '/upgrade/success': typeof UpgradeSuccessRoute '/app': typeof AppIndexRoute '/api/admin/add-runs': typeof ApiAdminAddRunsRoute + '/api/admin/custom-reviews': typeof ApiAdminCustomReviewsRoute '/api/admin/set-plan': typeof ApiAdminSetPlanRoute '/api/billing/checkout': typeof ApiBillingCheckoutRoute '/api/billing/webhook': typeof ApiBillingWebhookRoute @@ -320,6 +328,7 @@ export interface FileRoutesById { '/upgrade/success': typeof UpgradeSuccessRoute '/app/': typeof AppIndexRoute '/api/admin/add-runs': typeof ApiAdminAddRunsRoute + '/api/admin/custom-reviews': typeof ApiAdminCustomReviewsRoute '/api/admin/set-plan': typeof ApiAdminSetPlanRoute '/api/billing/checkout': typeof ApiBillingCheckoutRoute '/api/billing/webhook': typeof ApiBillingWebhookRoute @@ -360,6 +369,7 @@ export interface FileRouteTypes { | '/upgrade/success' | '/app/' | '/api/admin/add-runs' + | '/api/admin/custom-reviews' | '/api/admin/set-plan' | '/api/billing/checkout' | '/api/billing/webhook' @@ -394,6 +404,7 @@ export interface FileRouteTypes { | '/upgrade/success' | '/app' | '/api/admin/add-runs' + | '/api/admin/custom-reviews' | '/api/admin/set-plan' | '/api/billing/checkout' | '/api/billing/webhook' @@ -431,6 +442,7 @@ export interface FileRouteTypes { | '/upgrade/success' | '/app/' | '/api/admin/add-runs' + | '/api/admin/custom-reviews' | '/api/admin/set-plan' | '/api/billing/checkout' | '/api/billing/webhook' @@ -465,6 +477,7 @@ export interface RootRouteChildren { AuthSignUpRoute: typeof AuthSignUpRoute UpgradeSuccessRoute: typeof UpgradeSuccessRoute ApiAdminAddRunsRoute: typeof ApiAdminAddRunsRoute + ApiAdminCustomReviewsRoute: typeof ApiAdminCustomReviewsRoute ApiAdminSetPlanRoute: typeof ApiAdminSetPlanRoute ApiBillingCheckoutRoute: typeof ApiBillingCheckoutRoute ApiBillingWebhookRoute: typeof ApiBillingWebhookRoute @@ -617,6 +630,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiAdminAddRunsRouteImport parentRoute: typeof rootRouteImport } + '/api/admin/custom-reviews': { + id: '/api/admin/custom-reviews' + path: '/api/admin/custom-reviews' + fullPath: '/api/admin/custom-reviews' + preLoaderRoute: typeof ApiAdminCustomReviewsRouteImport + parentRoute: typeof rootRouteImport + } '/api/admin/set-plan': { id: '/api/admin/set-plan' path: '/api/admin/set-plan' @@ -822,6 +842,7 @@ const rootRouteChildren: RootRouteChildren = { AuthSignUpRoute: AuthSignUpRoute, UpgradeSuccessRoute: UpgradeSuccessRoute, ApiAdminAddRunsRoute: ApiAdminAddRunsRoute, + ApiAdminCustomReviewsRoute: ApiAdminCustomReviewsRoute, ApiAdminSetPlanRoute: ApiAdminSetPlanRoute, ApiBillingCheckoutRoute: ApiBillingCheckoutRoute, ApiBillingWebhookRoute: ApiBillingWebhookRoute, From 0f4fbdb9531730712996e401c4ab0aaa86b7d4ea Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Sat, 3 Oct 2026 05:12:50 +0100 Subject: [PATCH 13/13] feat(admin): grant custom reviews from the users table Add a "Custom reviews" column to the admin users table: - Free workspaces get a switch to grant or revoke the feature. It shows the current state, has a tooltip, is disabled while saving, and refreshes the table when the save succeeds. - Paid workspaces show "included", since their plan already has it. - Users without a workspace show nothing. The table's minimum width grows to fit the new column. --- src/components/admin/users-table.tsx | 71 +++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/src/components/admin/users-table.tsx b/src/components/admin/users-table.tsx index d5d0f3a..f27c9fe 100644 --- a/src/components/admin/users-table.tsx +++ b/src/components/admin/users-table.tsx @@ -25,7 +25,7 @@ export function AdminUsersTable({ users }: { users: AdminUserRow[] }) {
- +
@@ -35,6 +35,7 @@ export function AdminUsersTable({ users }: { users: AdminUserRow[] }) { + @@ -91,6 +92,20 @@ export function AdminUsersTable({ users }: { users: AdminUserRow[] }) { +
UserReviews Scans JoinedCustom reviews
{timeAgo(user.createdAt)} + {user.workspaceId && user.plan ? ( + user.plan === 'free' ? ( + + ) : ( + + included + + ) + ) : null} + {user.workspaceId && user.plan ? (
@@ -170,6 +185,60 @@ function PlanSelect({ ) } +// Grant or revoke custom review instructions for a free workspace. Paid plans +// include the feature, so they don't get a switch. +function CustomReviewsToggle({ + workspaceId, + granted, +}: { + workspaceId: string + granted: boolean +}) { + const router = useRouter() + const [busy, setBusy] = useState(false) + + async function toggle() { + setBusy(true) + try { + const res = await fetch('/api/admin/custom-reviews', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ workspaceId, granted: !granted }), + }) + if (res.ok) { + await router.invalidate() + } + } catch { + // ignore — the switch re-enables and the operator can retry + } finally { + setBusy(false) + } + } + + return ( + + ) +} + // Grant a free workspace one-time bonus runs for its current window. function RunsGrant({ workspaceId }: { workspaceId: string }) { const router = useRouter()