From a633877351ef84ddc87b0e8d53e57a7fbc94c789 Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Thu, 13 Aug 2026 19:52:31 +0100 Subject: [PATCH 01/26] feat(db): add page_views table for landing analytics Privacy-light tracking: path, country, referrer, created_at, with an index on created_at. No PII. --- src/db/schema.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/db/schema.ts b/src/db/schema.ts index b90ee21..0d067d8 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,5 +1,6 @@ import { boolean, + index, integer, jsonb, pgEnum, @@ -239,6 +240,26 @@ export const findings = pgTable('findings', { .defaultNow(), }) +// Lightweight, privacy-light page-view tracking for the public marketing pages +// (no PII โ€” just the path, the visitor's country from the edge geo header, and +// the referrer). Feeds the admin analytics dashboard. +export const pageViews = pgTable( + 'page_views', + { + id: uuid('id').primaryKey().defaultRandom(), + path: text('path').notNull(), + // ISO 3166-1 alpha-2 country code from the edge geo header; null if unknown. + country: text('country'), + referrer: text('referrer'), + createdAt: timestamp('created_at', { withTimezone: true }) + .notNull() + .defaultNow(), + }, + (table) => ({ + createdAtIdx: index('page_views_created_at_idx').on(table.createdAt), + }), +) + export const codebaseScans = pgTable('codebase_scans', { id: uuid('id').primaryKey().defaultRandom(), repositoryId: uuid('repository_id') From e0a511580eb22f2bb4a55b903dac87ae3582f0cd Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Thu, 13 Aug 2026 19:52:31 +0100 Subject: [PATCH 02/26] feat(db): generate page_views migration (0001) Incremental migration on top of the squashed baseline; creates page_views and its created_at index. --- drizzle/0001_jazzy_darkstar.sql | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 drizzle/0001_jazzy_darkstar.sql diff --git a/drizzle/0001_jazzy_darkstar.sql b/drizzle/0001_jazzy_darkstar.sql new file mode 100644 index 0000000..246928b --- /dev/null +++ b/drizzle/0001_jazzy_darkstar.sql @@ -0,0 +1,9 @@ +CREATE TABLE "page_views" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "path" text NOT NULL, + "country" text, + "referrer" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE INDEX "page_views_created_at_idx" ON "page_views" USING btree ("created_at"); \ No newline at end of file From 7db7ed34797da14aa56d0f59906b0fbbc83a81eb Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Thu, 13 Aug 2026 19:52:32 +0100 Subject: [PATCH 03/26] chore(db): add drizzle snapshot for migration 0001 --- drizzle/meta/0001_snapshot.json | 1057 +++++++++++++++++++++++++++++++ 1 file changed, 1057 insertions(+) create mode 100644 drizzle/meta/0001_snapshot.json diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..dc1e6d3 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,1057 @@ +{ + "id": "1f839194-0176-4cd4-b818-c454479087bc", + "prevId": "3e8f0707-68cb-4976-a972-9d27682feb66", + "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 + }, + "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 + }, + "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 + }, + "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 + }, + "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 + }, + "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 876322c5c82b42bb084fb7f7447b8a6bf1e59398 Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Thu, 13 Aug 2026 19:52:32 +0100 Subject: [PATCH 04/26] chore(db): record migration 0001 in the drizzle journal --- drizzle/meta/_journal.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 970edb1..fec878f 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1786571980089, "tag": "0000_small_mandarin", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1786644826598, + "tag": "0001_jazzy_darkstar", + "breakpoints": true } ] } \ No newline at end of file From 7777f74e1d9130e1494150559656a49bb4b8b699 Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Thu, 13 Aug 2026 19:52:33 +0100 Subject: [PATCH 05/26] feat(config): allow the ADMIN_USERNAMES env key Adds ADMIN_USERNAMES to OptionalEnvKey so the admin allowlist typechecks. --- src/server/env.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server/env.ts b/src/server/env.ts index fecf91a..cb2e525 100644 --- a/src/server/env.ts +++ b/src/server/env.ts @@ -12,6 +12,7 @@ type RequiredEnvKey = type OptionalEnvKey = | 'APP_URL' | 'NODE_ENV' + | 'ADMIN_USERNAMES' | 'LLM_PROVIDER' | 'LLM_MODEL' | 'GEMINI_API_KEY' From 82124c6154cbbb412dbd9280e362f128a4856d1a Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Thu, 13 Aug 2026 19:52:33 +0100 Subject: [PATCH 06/26] feat(admin): analytics + user-management server module requireAdmin/isAdminUsername gate every call via ADMIN_USERNAMES. getAdminOverview returns totals (users/workspaces/pro/free/MRR/reviews/scans/findings/views), 12-month trends, top countries, 30-day views, and a cross-workspace recent-activity feed. getAdminUsers lists users with plan + usage. setWorkspacePlanAsAdmin dispatches to billing. --- src/server/admin.ts | 256 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 src/server/admin.ts diff --git a/src/server/admin.ts b/src/server/admin.ts new file mode 100644 index 0000000..672421d --- /dev/null +++ b/src/server/admin.ts @@ -0,0 +1,256 @@ +// Operator/admin surface: platform-wide analytics and light user management. +// This exposes EVERY workspace's data, so access is gated server-side on every +// call (never trust the UI). Admins are named in the ADMIN_USERNAMES env var +// (comma-separated GitHub usernames). + +import { createServerFn } from '@tanstack/react-start' + +import { loadDb } from '../db/load' +import { PRO_PRICE_USD } from '../lib/plans' +import { getOptionalEnv } from './env' +import { getCurrentUserFromCookie } from './github-auth' +import type { CurrentUser } from './github-auth' + +export function isAdminUsername(username: string): boolean { + const allow = getOptionalEnv('ADMIN_USERNAMES', '') + .split(',') + .map((entry) => entry.trim().toLowerCase()) + .filter(Boolean) + return allow.includes(username.toLowerCase()) +} + +/** Throws unless the signed-in user is an admin. Returns the admin user. */ +export async function requireAdmin(): Promise { + const user = await getCurrentUserFromCookie() + if (!user || !isAdminUsername(user.username)) { + throw new Error('forbidden') + } + return user +} + +// Used by the route's beforeLoad to gate access (returns null instead of +// throwing so the route can redirect cleanly). The env check stays server-side. +export const getAdminContext = createServerFn({ method: 'GET' }).handler( + async (): Promise<{ username: string } | null> => { + const user = await getCurrentUserFromCookie() + if (!user || !isAdminUsername(user.username)) { + return null + } + return { username: user.username } + }, +) + +export type AdminOverview = { + totals: { + users: number + workspaces: number + pro: number + free: number + mrrUsd: number + reviews: number + scans: number + findings: number + landingViews: number + } + trends: Array<{ + month: string + signups: number + reviews: number + scans: number + }> + countries: Array<{ country: string; count: number }> + viewsByDay: Array<{ day: string; count: number }> + recent: Array<{ + type: 'review' | 'scan' + repository: string + workspace: string + status: string + at: string + }> +} + +function lastMonths(n: number): string[] { + const out: string[] = [] + const now = new Date() + for (let i = n - 1; i >= 0; i--) { + const m = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - i, 1)) + out.push( + `${m.getUTCFullYear()}-${String(m.getUTCMonth() + 1).padStart(2, '0')}`, + ) + } + return out +} + +export const getAdminOverview = createServerFn({ method: 'GET' }).handler( + async (): Promise => { + await requireAdmin() + const { sqlClient } = await loadDb() + + const [ + totals, + signups, + reviewsByMonth, + scansByMonth, + countries, + byDay, + recent, + ] = await Promise.all([ + sqlClient` + select + (select count(*)::int from users) as users, + (select count(*)::int from workspaces) as workspaces, + (select count(*)::int from workspaces where plan = 'pro') as pro, + (select count(*)::int from review_runs) as reviews, + (select count(*)::int from codebase_scans) as scans, + (select count(*)::int from findings) as findings, + (select count(*)::int from page_views) as views`, + sqlClient` + select to_char(date_trunc('month', created_at), 'YYYY-MM') as month, count(*)::int as count + from users where created_at > now() - interval '12 months' + group by 1`, + sqlClient` + select to_char(date_trunc('month', created_at), 'YYYY-MM') as month, count(*)::int as count + from review_runs where created_at > now() - interval '12 months' + group by 1`, + sqlClient` + select to_char(date_trunc('month', created_at), 'YYYY-MM') as month, count(*)::int as count + from codebase_scans where created_at > now() - interval '12 months' + group by 1`, + sqlClient` + select coalesce(country, '??') as country, count(*)::int as count + from page_views group by 1 order by count desc limit 8`, + sqlClient` + select to_char(date_trunc('day', created_at), 'YYYY-MM-DD') as day, count(*)::int as count + from page_views where created_at > now() - interval '30 days' + group by 1 order by 1`, + sqlClient` + (select 'review' as type, r.owner || '/' || r.name as repository, w.slug as workspace, + rr.status::text as status, rr.created_at as at + from review_runs rr + join pull_requests pr on pr.id = rr.pull_request_id + join repositories r on r.id = pr.repository_id + join workspaces w on w.id = r.workspace_id) + union all + (select 'scan' as type, r.owner || '/' || r.name as repository, w.slug as workspace, + cs.status::text as status, cs.created_at as at + from codebase_scans cs + join repositories r on r.id = cs.repository_id + join workspaces w on w.id = r.workspace_id) + order by at desc limit 12`, + ]) + + const t = totals[0] + const pro = Number(t.pro ?? 0) + const workspaces = Number(t.workspaces ?? 0) + + const signupMap = new Map(signups.map((r) => [r.month, Number(r.count)])) + const reviewMap = new Map( + reviewsByMonth.map((r) => [r.month, Number(r.count)]), + ) + const scanMap = new Map(scansByMonth.map((r) => [r.month, Number(r.count)])) + + return { + totals: { + users: Number(t.users ?? 0), + workspaces, + pro, + free: Math.max(0, workspaces - pro), + mrrUsd: pro * PRO_PRICE_USD, + reviews: Number(t.reviews ?? 0), + scans: Number(t.scans ?? 0), + findings: Number(t.findings ?? 0), + landingViews: Number(t.views ?? 0), + }, + trends: lastMonths(12).map((month) => ({ + month, + signups: signupMap.get(month) ?? 0, + reviews: reviewMap.get(month) ?? 0, + scans: scanMap.get(month) ?? 0, + })), + countries: countries.map((r) => ({ + country: String(r.country), + count: Number(r.count), + })), + viewsByDay: byDay.map((r) => ({ + day: String(r.day), + count: Number(r.count), + })), + recent: recent.map((r) => ({ + type: r.type === 'scan' ? 'scan' : 'review', + repository: String(r.repository), + workspace: String(r.workspace), + status: String(r.status), + at: new Date(r.at).toISOString(), + })), + } + }, +) + +export type AdminUserRow = { + userId: string + username: string + name: string | null + email: string | null + avatarUrl: string | null + createdAt: string + workspaceId: string | null + workspaceName: string | null + plan: 'free' | 'pro' | null + runsUsed: number + repos: number + reviews: number + scans: number +} + +export const getAdminUsers = createServerFn({ method: 'GET' }).handler( + async (): Promise => { + await requireAdmin() + const { sqlClient } = await loadDb() + + 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, + (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 + join repositories rp on rp.id = pr.repository_id + where rp.workspace_id = w.id) as reviews, + (select count(*)::int from codebase_scans cs + join repositories rp on rp.id = cs.repository_id + where rp.workspace_id = w.id) as scans + from users u + left join workspaces w on w.owner_id = u.id + order by u.created_at desc` + + return rows.map((r) => ({ + userId: String(r.user_id), + username: String(r.username), + name: r.name ? String(r.name) : null, + email: r.email ? String(r.email) : null, + avatarUrl: r.avatar_url ? String(r.avatar_url) : null, + createdAt: new Date(r.created_at).toISOString(), + workspaceId: r.workspace_id ? String(r.workspace_id) : null, + workspaceName: r.workspace_name ? String(r.workspace_name) : null, + plan: r.plan === 'pro' ? 'pro' : r.workspace_id ? 'free' : null, + runsUsed: Number(r.runs_used ?? 0), + repos: Number(r.repos ?? 0), + reviews: Number(r.reviews ?? 0), + scans: Number(r.scans ?? 0), + })) + }, +) + +// Admin-only plan override. Kept here (not the API route) so the gate and the +// dispatch live together; the API route calls this after re-checking the admin. +export async function setWorkspacePlanAsAdmin( + workspaceId: string, + plan: 'free' | 'pro', +): Promise { + const { markWorkspacePro, downgradeWorkspace } = await import('./billing') + if (plan === 'pro') { + await markWorkspacePro(workspaceId) + } else { + await downgradeWorkspace(workspaceId) + } +} From d8dffebb080cc0a34104cfb706ac864bf42beaa9 Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Thu, 13 Aug 2026 19:52:34 +0100 Subject: [PATCH 07/26] feat(lib): add countryFlag helper Pure ISO 3166-1 alpha-2 -> flag emoji, with a globe fallback. --- src/lib/country.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/lib/country.ts diff --git a/src/lib/country.ts b/src/lib/country.ts new file mode 100644 index 0000000..1f37972 --- /dev/null +++ b/src/lib/country.ts @@ -0,0 +1,10 @@ +// Turn an ISO 3166-1 alpha-2 country code into its flag emoji. Unknown or +// malformed codes fall back to a globe. Pure โ€” no I/O. +export function countryFlag(code: string): string { + if (!/^[A-Za-z]{2}$/.test(code)) { + return '๐ŸŒ' + } + return String.fromCodePoint( + ...[...code.toUpperCase()].map((char) => 127397 + char.charCodeAt(0)), + ) +} From ba23dacddcd5f10c36173259326857fcef4351d6 Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Thu, 13 Aug 2026 19:52:46 +0100 Subject: [PATCH 08/26] feat(admin): add shared chart mount-guard hook useMounted defers recharts rendering until after mount to avoid an SSR/hydration size mismatch. --- src/components/admin/use-mounted.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/components/admin/use-mounted.ts diff --git a/src/components/admin/use-mounted.ts b/src/components/admin/use-mounted.ts new file mode 100644 index 0000000..ea8d819 --- /dev/null +++ b/src/components/admin/use-mounted.ts @@ -0,0 +1,9 @@ +import { useEffect, useState } from 'react' + +// Recharts sizes its ResponsiveContainer from the DOM, so charts must render +// only after mount to avoid an SSR/hydration size mismatch. +export function useMounted(): boolean { + const [mounted, setMounted] = useState(false) + useEffect(() => setMounted(true), []) + return mounted +} From 85b08b3181be2f1893e517fc1bda0d38570a35e5 Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Thu, 13 Aug 2026 19:52:47 +0100 Subject: [PATCH 09/26] feat(admin): add overview stat tiles --- src/components/admin/stats.tsx | 60 ++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/components/admin/stats.tsx diff --git a/src/components/admin/stats.tsx b/src/components/admin/stats.tsx new file mode 100644 index 0000000..9dd2a04 --- /dev/null +++ b/src/components/admin/stats.tsx @@ -0,0 +1,60 @@ +import { + BadgeCheck, + DollarSign, + FileWarning, + Globe, + LayoutDashboard, + ScanSearch, + Users as UsersIcon, +} from 'lucide-react' + +import type { AdminOverview } from '../../server/admin' + +// Platform-wide overview tiles: users, workspaces (pro/free), MRR, agent +// activity, and landing views. +export function AdminStats({ totals }: { totals: AdminOverview['totals'] }) { + const cards = [ + { label: 'users', value: String(totals.users), icon: UsersIcon }, + { + label: 'workspaces', + value: String(totals.workspaces), + sub: `${totals.pro} pro ยท ${totals.free} free`, + icon: LayoutDashboard, + }, + { + label: 'MRR', + value: `$${totals.mrrUsd.toLocaleString()}`, + sub: `${totals.pro} ร— $15`, + icon: DollarSign, + }, + { label: 'reviews', value: String(totals.reviews), icon: BadgeCheck }, + { label: 'scans', value: String(totals.scans), icon: ScanSearch }, + { label: 'findings', value: String(totals.findings), icon: FileWarning }, + { + label: 'landing views', + value: totals.landingViews.toLocaleString(), + icon: Globe, + }, + ] + + return ( +
+ {cards.map(({ label, value, sub, icon: Icon }) => ( +
+
+ +

+ {label} +

+
+

+ {value} +

+ {sub ? ( +

{sub}

+ ) : null} +
+ ))} +
+ ) +} From 70feae0e58e408c840140fa7770017d7fd668910 Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Thu, 13 Aug 2026 19:52:48 +0100 Subject: [PATCH 10/26] feat(admin): add 12-month growth & activity chart --- src/components/admin/trends-chart.tsx | 132 ++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 src/components/admin/trends-chart.tsx diff --git a/src/components/admin/trends-chart.tsx b/src/components/admin/trends-chart.tsx new file mode 100644 index 0000000..eb49756 --- /dev/null +++ b/src/components/admin/trends-chart.tsx @@ -0,0 +1,132 @@ +import { + Area, + AreaChart, + CartesianGrid, + Legend, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts' + +import type { AdminOverview } from '../../server/admin' +import { useMounted } from './use-mounted' + +const SIGNUP_COLOR = '#34d399' // emerald โ€” signups +const REVIEW_COLOR = '#d97706' // amber โ€” reviews +const SCAN_COLOR = '#0284c7' // sky โ€” scans + +// Monthly signups / reviews / scans over the last 12 months. +export function AdminTrendsChart({ data }: { data: AdminOverview['trends'] }) { + const mounted = useMounted() + const hasData = data.some( + (point) => point.signups + point.reviews + point.scans > 0, + ) + + return ( +
+

+ Growth & activity (12 months) +

+ {!hasData ? ( +

+ No activity recorded yet. +

+ ) : mounted ? ( +
+ + + + {[ + ['grad-signups', SIGNUP_COLOR], + ['grad-reviews2', REVIEW_COLOR], + ['grad-scans2', SCAN_COLOR], + ].map(([id, color]) => ( + + + + + ))} + + + + + + + + + + + +
+ ) : ( +
+ )} +
+ ) +} From b314047740461f080cda3a4160e7b736a08d8bb5 Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Thu, 13 Aug 2026 19:52:49 +0100 Subject: [PATCH 11/26] feat(admin): add 30-day landing-views chart --- src/components/admin/views-chart.tsx | 88 ++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/components/admin/views-chart.tsx diff --git a/src/components/admin/views-chart.tsx b/src/components/admin/views-chart.tsx new file mode 100644 index 0000000..e700050 --- /dev/null +++ b/src/components/admin/views-chart.tsx @@ -0,0 +1,88 @@ +import { + Area, + AreaChart, + CartesianGrid, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts' + +import { Globe } from 'lucide-react' + +import type { AdminOverview } from '../../server/admin' +import { useMounted } from './use-mounted' + +// Landing-page views per day over the last 30 days. +export function AdminViewsChart({ + data, +}: { + data: AdminOverview['viewsByDay'] +}) { + const mounted = useMounted() + const hasData = data.some((point) => point.count > 0) + + return ( +
+
+ +

+ Landing views (30 days) +

+
+ {!hasData ? ( +

No views yet.

+ ) : mounted ? ( +
+ + + + + + + + + + + + + + + +
+ ) : ( +
+ )} +
+ ) +} From b424f7bca8d48da67064297d2858aac30ab6d00c Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Thu, 13 Aug 2026 19:52:49 +0100 Subject: [PATCH 12/26] feat(admin): add top-countries breakdown --- src/components/admin/countries.tsx | 54 ++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/components/admin/countries.tsx diff --git a/src/components/admin/countries.tsx b/src/components/admin/countries.tsx new file mode 100644 index 0000000..789aab6 --- /dev/null +++ b/src/components/admin/countries.tsx @@ -0,0 +1,54 @@ +import { Globe } from 'lucide-react' + +import { countryFlag } from '../../lib/country' +import type { AdminOverview } from '../../server/admin' + +// Top landing-page visitor countries, as share-of-total bars. +export function AdminCountries({ + countries, + total, +}: { + countries: AdminOverview['countries'] + total: number +}) { + const max = Math.max(1, ...countries.map((entry) => entry.count)) + + return ( +
+
+ +

+ Top countries +

+
+ {countries.length === 0 ? ( +

No views yet.

+ ) : ( +
    + {countries.map((entry) => ( +
  • + + {countryFlag(entry.country)} + + + {entry.country} + +
    +
    +
    + + {entry.count} + + + {total > 0 ? Math.round((entry.count / total) * 100) : 0}% + +
  • + ))} +
+ )} +
+ ) +} From 36a06b827870581d4e2d78da76b43129f2256502 Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Thu, 13 Aug 2026 19:52:50 +0100 Subject: [PATCH 13/26] feat(admin): add users table with plan toggle Lists every user with plan + usage; the Make Pro/Free button posts to /api/admin/set-plan and revalidates. --- src/components/admin/users-table.tsx | 153 +++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 src/components/admin/users-table.tsx diff --git a/src/components/admin/users-table.tsx b/src/components/admin/users-table.tsx new file mode 100644 index 0000000..a62ec3d --- /dev/null +++ b/src/components/admin/users-table.tsx @@ -0,0 +1,153 @@ +import { useRouter } from '@tanstack/react-router' +import { Users as UsersIcon } from 'lucide-react' +import { useState } from 'react' + +import { timeAgo } from '../../lib/format' +import type { AdminUserRow } from '../../server/admin' + +// Every user with their workspace plan + usage, and an inline plan toggle. +export function AdminUsersTable({ users }: { users: AdminUserRow[] }) { + return ( +
+
+ +

+ Users ({users.length}) +

+
+ +
+ + + + + + + + + + + + + + {users.map((user) => ( + + + + + + + + + + + ))} + +
UserPlanRunsReposReviewsScansJoined +
+
+ {user.avatarUrl ? ( + + ) : ( + + )} +
+

+ {user.username} +

+

+ {user.email ?? 'no email'} +

+
+
+
+ + + {user.runsUsed} + + {user.repos} + + {user.reviews} + + {user.scans} + + {timeAgo(user.createdAt)} + + {user.workspaceId && user.plan ? ( + + ) : ( + + no workspace + + )} +
+
+
+ ) +} + +function PlanBadge({ plan }: { plan: 'free' | 'pro' | null }) { + if (plan === 'pro') { + return ( + + pro + + ) + } + return ( + + {plan ?? 'none'} + + ) +} + +function PlanToggle({ + workspaceId, + plan, +}: { + workspaceId: string + plan: 'free' | 'pro' +}) { + const router = useRouter() + const [busy, setBusy] = useState(false) + const next = plan === 'pro' ? 'free' : 'pro' + + async function toggle() { + setBusy(true) + try { + const res = await fetch('/api/admin/set-plan', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ workspaceId, plan: next }), + }) + if (res.ok) { + await router.invalidate() + } + } catch { + // ignore โ€” the button re-enables and the operator can retry + } finally { + setBusy(false) + } + } + + return ( + + ) +} From d5cfac412705bce57c3d9e89bc3fcbefa594e011 Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Thu, 13 Aug 2026 19:52:51 +0100 Subject: [PATCH 14/26] feat(admin): add recent-activity feed --- src/components/admin/recent-activity.tsx | 56 ++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/components/admin/recent-activity.tsx diff --git a/src/components/admin/recent-activity.tsx b/src/components/admin/recent-activity.tsx new file mode 100644 index 0000000..49d4688 --- /dev/null +++ b/src/components/admin/recent-activity.tsx @@ -0,0 +1,56 @@ +import { BadgeCheck, ScanSearch } from 'lucide-react' + +import { timeAgo } from '../../lib/format' +import type { AdminOverview } from '../../server/admin' + +// The latest reviews and scans across all workspaces. +export function AdminRecentActivity({ + recent, +}: { + recent: AdminOverview['recent'] +}) { + return ( +
+

+ Recent activity +

+ {recent.length === 0 ? ( +

No activity yet.

+ ) : ( +
    + {recent.map((item, index) => ( +
  • + + {item.type === 'review' ? ( + + ) : ( + + )} + +
    +

    + {item.repository} +

    +

    + {item.workspace} ยท {item.type} ยท {item.status} +

    +
    + + {timeAgo(item.at)} + +
  • + ))} +
+ )} +
+ ) +} From f1807b0000ae8faa1ea2d6fba10a1c9157368bac Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Thu, 13 Aug 2026 19:52:52 +0100 Subject: [PATCH 15/26] feat(analytics): add page-view beacon component Fires a single sendBeacon/fetch on mount with just path + referrer. --- src/components/page-view.tsx | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/components/page-view.tsx diff --git a/src/components/page-view.tsx b/src/components/page-view.tsx new file mode 100644 index 0000000..8f086f7 --- /dev/null +++ b/src/components/page-view.tsx @@ -0,0 +1,36 @@ +import { useEffect, useRef } from 'react' + +// Fires a single privacy-light page-view beacon on mount (just the path and +// referrer). The visitor's country is derived server-side from the edge geo +// header โ€” nothing identifying is sent from the client. Renders nothing. +export function PageView({ path }: { path: string }) { + const sent = useRef(false) + + useEffect(() => { + if (sent.current) return + sent.current = true + + const body = JSON.stringify({ + path, + referrer: + typeof document !== 'undefined' ? document.referrer || null : null, + }) + + // Prefer sendBeacon so the request survives navigation; fall back to fetch. + try { + const blob = new Blob([body], { type: 'application/json' }) + if (navigator.sendBeacon('/api/track', blob)) return + } catch { + // sendBeacon unavailable โ€” fall through to fetch + } + + void fetch('/api/track', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body, + keepalive: true, + }).catch(() => {}) + }, [path]) + + return null +} From e6295a9d369bdb3ac463f59d36971855a27e0ba1 Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Thu, 13 Aug 2026 19:57:38 +0100 Subject: [PATCH 16/26] feat(analytics): add page-view ingest endpoint POST /api/track records path + referrer and derives country from the edge geo header (x-vercel-ip-country / cf-ipcountry). Best-effort, always 204. --- src/routes/api.track.tsx | 45 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/routes/api.track.tsx diff --git a/src/routes/api.track.tsx b/src/routes/api.track.tsx new file mode 100644 index 0000000..6db8737 --- /dev/null +++ b/src/routes/api.track.tsx @@ -0,0 +1,45 @@ +import { createFileRoute } from '@tanstack/react-router' + +import { loadDb } from '../db/load' + +// Records a public page view. Best-effort and privacy-light: only the path, +// referrer, and the visitor's country (from the edge geo header) are stored โ€” +// no cookies, no IP, no user id. Always returns 204 so tracking never breaks a +// page load. +export const Route = createFileRoute('/api/track')({ + server: { + handlers: { + POST: async ({ request }) => { + let body: { path?: unknown; referrer?: unknown } = {} + try { + body = (await request.json()) as typeof body + } catch { + return new Response(null, { status: 204 }) + } + + const path = + typeof body.path === 'string' ? body.path.slice(0, 512) : '' + if (!path) { + return new Response(null, { status: 204 }) + } + + const referrer = + typeof body.referrer === 'string' ? body.referrer.slice(0, 512) : null + // Vercel/edge geo header (ISO alpha-2). Absent locally โ†’ null. + const country = + request.headers.get('x-vercel-ip-country') || + request.headers.get('cf-ipcountry') || + null + + try { + const { db, pageViews } = await loadDb() + await db.insert(pageViews).values({ path, referrer, country }) + } catch (error) { + console.error('[track] failed to record page view', error) + } + + return new Response(null, { status: 204 }) + }, + }, + }, +}) From ca28609573875bbdba0fa2057e8dd7310a430305 Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Thu, 13 Aug 2026 19:57:38 +0100 Subject: [PATCH 17/26] feat(admin): add plan-change API route POST /api/admin/set-plan re-checks the admin via getCurrentUserFromRequest and flips a workspace between free and pro. --- src/routes/api.admin.set-plan.tsx | 50 +++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/routes/api.admin.set-plan.tsx diff --git a/src/routes/api.admin.set-plan.tsx b/src/routes/api.admin.set-plan.tsx new file mode 100644 index 0000000..d2a4e69 --- /dev/null +++ b/src/routes/api.admin.set-plan.tsx @@ -0,0 +1,50 @@ +import { createFileRoute } from '@tanstack/react-router' + +import { isAdminUsername, setWorkspacePlanAsAdmin } from '../server/admin' +import { getCurrentUserFromRequest } from '../server/github-auth' + +// Admin-only: manually set a workspace's plan (free โ†” pro). Re-checks the admin +// on the server; the UI gate is never trusted. +export const Route = createFileRoute('/api/admin/set-plan')({ + server: { + handlers: { + POST: async ({ request }) => { + const user = await getCurrentUserFromRequest(request) + if (!user || !isAdminUsername(user.username)) { + return json({ error: 'forbidden' }, 403) + } + + let body: { workspaceId?: unknown; plan?: unknown } = {} + try { + body = (await request.json()) as typeof body + } catch { + return json({ error: 'invalid body' }, 400) + } + + const workspaceId = + typeof body.workspaceId === 'string' ? body.workspaceId : '' + const plan = + body.plan === 'pro' ? 'pro' : body.plan === 'free' ? 'free' : null + + if (!workspaceId || !plan) { + return json({ error: 'workspaceId and plan are required' }, 400) + } + + try { + await setWorkspacePlanAsAdmin(workspaceId, plan) + return json({ ok: true, plan }, 200) + } catch (error) { + console.error('[admin] set-plan failed', error) + return json({ error: 'Unable to update plan' }, 500) + } + }, + }, + }, +}) + +function json(body: unknown, status: number) { + return new Response(JSON.stringify(body), { + status, + headers: { 'content-type': 'application/json' }, + }) +} From 4798134dc99d9422477f0d541fc1591a9c11a7d1 Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Thu, 13 Aug 2026 19:57:39 +0100 Subject: [PATCH 18/26] feat(admin): add operator dashboard route /admin (noindex, gated in beforeLoad; non-admins redirect to /app). Thin route: loads the overview + users and composes the admin components. --- src/routes/admin.tsx | 78 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/routes/admin.tsx diff --git a/src/routes/admin.tsx b/src/routes/admin.tsx new file mode 100644 index 0000000..34f1b94 --- /dev/null +++ b/src/routes/admin.tsx @@ -0,0 +1,78 @@ +import { createFileRoute, Link, redirect } from '@tanstack/react-router' +import { ArrowLeft } from 'lucide-react' + +import { AdminCountries } from '../components/admin/countries' +import { AdminRecentActivity } from '../components/admin/recent-activity' +import { AdminStats } from '../components/admin/stats' +import { AdminTrendsChart } from '../components/admin/trends-chart' +import { AdminUsersTable } from '../components/admin/users-table' +import { AdminViewsChart } from '../components/admin/views-chart' +import { + getAdminContext, + getAdminOverview, + getAdminUsers, +} from '../server/admin' + +export const Route = createFileRoute('/admin')({ + head: () => ({ + meta: [{ name: 'robots', content: 'noindex, nofollow' }], + }), + beforeLoad: async () => { + const ctx = await getAdminContext() + if (!ctx) { + throw redirect({ to: '/app' }) + } + return { admin: ctx } + }, + loader: async () => { + const [overview, users] = await Promise.all([ + getAdminOverview(), + getAdminUsers(), + ]) + return { overview, users } + }, + component: AdminPage, +}) + +function AdminPage() { + const { overview, users } = Route.useLoaderData() + + return ( +
+
+
+
+

+ operator +

+

+ Admin +

+
+ + + App + +
+ + + + +
+ + +
+ + + +
+
+ ) +} From ef5abf893a3b66358f9f7044fd371cec3821933f Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Thu, 13 Aug 2026 19:57:39 +0100 Subject: [PATCH 19/26] feat(landing): fire a page-view beacon on the landing page --- src/routes/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routes/index.tsx b/src/routes/index.tsx index ac288d6..a8667ea 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -17,6 +17,7 @@ import { import { useState } from 'react' import { OwlMark } from '../components/owl-mark' +import { PageView } from '../components/page-view' import { featureItems, footerColumns, @@ -440,6 +441,7 @@ function Home() { return (
+
From 7c9eb7a19049c74477e45243b204a5c884b84e62 Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Thu, 13 Aug 2026 19:57:40 +0100 Subject: [PATCH 20/26] chore(routes): regenerate route tree for admin + tracking routes --- src/routeTree.gen.ts | 63 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 91af77b..2cfa073 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -15,6 +15,7 @@ import { Route as PricingRouteImport } from './routes/pricing' import { Route as OnboardingRouteImport } from './routes/onboarding' import { Route as DocsRouteImport } from './routes/docs' import { Route as AppRouteImport } from './routes/app' +import { Route as AdminRouteImport } from './routes/admin' import { Route as IndexRouteImport } from './routes/index' import { Route as AppIndexRouteImport } from './routes/app.index' import { Route as UpgradeSuccessRouteImport } from './routes/upgrade.success' @@ -26,6 +27,7 @@ import { Route as AppSettingsRouteImport } from './routes/app.settings' import { Route as AppScansRouteImport } from './routes/app.scans' import { Route as AppReviewsRouteImport } from './routes/app.reviews' import { Route as AppRepositoriesRouteImport } from './routes/app.repositories' +import { Route as ApiTrackRouteImport } from './routes/api.track' import { Route as AppScansIndexRouteImport } from './routes/app.scans.index' import { Route as AppReviewsIndexRouteImport } from './routes/app.reviews.index' import { Route as AppRepositoriesIndexRouteImport } from './routes/app.repositories.index' @@ -39,6 +41,7 @@ import { Route as ApiScansStartRouteImport } from './routes/api.scans.start' import { Route as ApiGithubWebhookRouteImport } from './routes/api.github.webhook' import { Route as ApiBillingWebhookRouteImport } from './routes/api.billing.webhook' import { Route as ApiBillingCheckoutRouteImport } from './routes/api.billing.checkout' +import { Route as ApiAdminSetPlanRouteImport } from './routes/api.admin.set-plan' const TermsRoute = TermsRouteImport.update({ id: '/terms', @@ -70,6 +73,11 @@ const AppRoute = AppRouteImport.update({ path: '/app', getParentRoute: () => rootRouteImport, } as any) +const AdminRoute = AdminRouteImport.update({ + id: '/admin', + path: '/admin', + getParentRoute: () => rootRouteImport, +} as any) const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', @@ -125,6 +133,11 @@ const AppRepositoriesRoute = AppRepositoriesRouteImport.update({ path: '/repositories', getParentRoute: () => AppRoute, } as any) +const ApiTrackRoute = ApiTrackRouteImport.update({ + id: '/api/track', + path: '/api/track', + getParentRoute: () => rootRouteImport, +} as any) const AppScansIndexRoute = AppScansIndexRouteImport.update({ id: '/', path: '/', @@ -190,15 +203,22 @@ const ApiBillingCheckoutRoute = ApiBillingCheckoutRouteImport.update({ path: '/api/billing/checkout', getParentRoute: () => rootRouteImport, } as any) +const ApiAdminSetPlanRoute = ApiAdminSetPlanRouteImport.update({ + id: '/api/admin/set-plan', + path: '/api/admin/set-plan', + getParentRoute: () => rootRouteImport, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute + '/admin': typeof AdminRoute '/app': typeof AppRouteWithChildren '/docs': typeof DocsRoute '/onboarding': typeof OnboardingRoute '/pricing': typeof PricingRoute '/privacy': typeof PrivacyRoute '/terms': typeof TermsRoute + '/api/track': typeof ApiTrackRoute '/app/repositories': typeof AppRepositoriesRouteWithChildren '/app/reviews': typeof AppReviewsRouteWithChildren '/app/scans': typeof AppScansRouteWithChildren @@ -209,6 +229,7 @@ export interface FileRoutesByFullPath { '/auth/sign-up': typeof AuthSignUpRoute '/upgrade/success': typeof UpgradeSuccessRoute '/app/': typeof AppIndexRoute + '/api/admin/set-plan': typeof ApiAdminSetPlanRoute '/api/billing/checkout': typeof ApiBillingCheckoutRoute '/api/billing/webhook': typeof ApiBillingWebhookRoute '/api/github/webhook': typeof ApiGithubWebhookRoute @@ -225,11 +246,13 @@ export interface FileRoutesByFullPath { } export interface FileRoutesByTo { '/': typeof IndexRoute + '/admin': typeof AdminRoute '/docs': typeof DocsRoute '/onboarding': typeof OnboardingRoute '/pricing': typeof PricingRoute '/privacy': typeof PrivacyRoute '/terms': typeof TermsRoute + '/api/track': typeof ApiTrackRoute '/app/settings': typeof AppSettingsRoute '/auth/connect': typeof AuthConnectRoute '/auth/setup': typeof AuthSetupRoute @@ -237,6 +260,7 @@ export interface FileRoutesByTo { '/auth/sign-up': typeof AuthSignUpRoute '/upgrade/success': typeof UpgradeSuccessRoute '/app': typeof AppIndexRoute + '/api/admin/set-plan': typeof ApiAdminSetPlanRoute '/api/billing/checkout': typeof ApiBillingCheckoutRoute '/api/billing/webhook': typeof ApiBillingWebhookRoute '/api/github/webhook': typeof ApiGithubWebhookRoute @@ -254,12 +278,14 @@ export interface FileRoutesByTo { export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute + '/admin': typeof AdminRoute '/app': typeof AppRouteWithChildren '/docs': typeof DocsRoute '/onboarding': typeof OnboardingRoute '/pricing': typeof PricingRoute '/privacy': typeof PrivacyRoute '/terms': typeof TermsRoute + '/api/track': typeof ApiTrackRoute '/app/repositories': typeof AppRepositoriesRouteWithChildren '/app/reviews': typeof AppReviewsRouteWithChildren '/app/scans': typeof AppScansRouteWithChildren @@ -270,6 +296,7 @@ export interface FileRoutesById { '/auth/sign-up': typeof AuthSignUpRoute '/upgrade/success': typeof UpgradeSuccessRoute '/app/': typeof AppIndexRoute + '/api/admin/set-plan': typeof ApiAdminSetPlanRoute '/api/billing/checkout': typeof ApiBillingCheckoutRoute '/api/billing/webhook': typeof ApiBillingWebhookRoute '/api/github/webhook': typeof ApiGithubWebhookRoute @@ -288,12 +315,14 @@ export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: | '/' + | '/admin' | '/app' | '/docs' | '/onboarding' | '/pricing' | '/privacy' | '/terms' + | '/api/track' | '/app/repositories' | '/app/reviews' | '/app/scans' @@ -304,6 +333,7 @@ export interface FileRouteTypes { | '/auth/sign-up' | '/upgrade/success' | '/app/' + | '/api/admin/set-plan' | '/api/billing/checkout' | '/api/billing/webhook' | '/api/github/webhook' @@ -320,11 +350,13 @@ export interface FileRouteTypes { fileRoutesByTo: FileRoutesByTo to: | '/' + | '/admin' | '/docs' | '/onboarding' | '/pricing' | '/privacy' | '/terms' + | '/api/track' | '/app/settings' | '/auth/connect' | '/auth/setup' @@ -332,6 +364,7 @@ export interface FileRouteTypes { | '/auth/sign-up' | '/upgrade/success' | '/app' + | '/api/admin/set-plan' | '/api/billing/checkout' | '/api/billing/webhook' | '/api/github/webhook' @@ -348,12 +381,14 @@ export interface FileRouteTypes { id: | '__root__' | '/' + | '/admin' | '/app' | '/docs' | '/onboarding' | '/pricing' | '/privacy' | '/terms' + | '/api/track' | '/app/repositories' | '/app/reviews' | '/app/scans' @@ -364,6 +399,7 @@ export interface FileRouteTypes { | '/auth/sign-up' | '/upgrade/success' | '/app/' + | '/api/admin/set-plan' | '/api/billing/checkout' | '/api/billing/webhook' | '/api/github/webhook' @@ -381,17 +417,20 @@ export interface FileRouteTypes { } export interface RootRouteChildren { IndexRoute: typeof IndexRoute + AdminRoute: typeof AdminRoute AppRoute: typeof AppRouteWithChildren DocsRoute: typeof DocsRoute OnboardingRoute: typeof OnboardingRoute PricingRoute: typeof PricingRoute PrivacyRoute: typeof PrivacyRoute TermsRoute: typeof TermsRoute + ApiTrackRoute: typeof ApiTrackRoute AuthConnectRoute: typeof AuthConnectRoute AuthSetupRoute: typeof AuthSetupRoute AuthSignInRoute: typeof AuthSignInRoute AuthSignUpRoute: typeof AuthSignUpRoute UpgradeSuccessRoute: typeof UpgradeSuccessRoute + ApiAdminSetPlanRoute: typeof ApiAdminSetPlanRoute ApiBillingCheckoutRoute: typeof ApiBillingCheckoutRoute ApiBillingWebhookRoute: typeof ApiBillingWebhookRoute ApiGithubWebhookRoute: typeof ApiGithubWebhookRoute @@ -445,6 +484,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AppRouteImport parentRoute: typeof rootRouteImport } + '/admin': { + id: '/admin' + path: '/admin' + fullPath: '/admin' + preLoaderRoute: typeof AdminRouteImport + parentRoute: typeof rootRouteImport + } '/': { id: '/' path: '/' @@ -522,6 +568,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AppRepositoriesRouteImport parentRoute: typeof AppRoute } + '/api/track': { + id: '/api/track' + path: '/api/track' + fullPath: '/api/track' + preLoaderRoute: typeof ApiTrackRouteImport + parentRoute: typeof rootRouteImport + } '/app/scans/': { id: '/app/scans/' path: '/' @@ -613,6 +666,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiBillingCheckoutRouteImport parentRoute: typeof rootRouteImport } + '/api/admin/set-plan': { + id: '/api/admin/set-plan' + path: '/api/admin/set-plan' + fullPath: '/api/admin/set-plan' + preLoaderRoute: typeof ApiAdminSetPlanRouteImport + parentRoute: typeof rootRouteImport + } } } @@ -678,17 +738,20 @@ const AppRouteWithChildren = AppRoute._addFileChildren(AppRouteChildren) const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, + AdminRoute: AdminRoute, AppRoute: AppRouteWithChildren, DocsRoute: DocsRoute, OnboardingRoute: OnboardingRoute, PricingRoute: PricingRoute, PrivacyRoute: PrivacyRoute, TermsRoute: TermsRoute, + ApiTrackRoute: ApiTrackRoute, AuthConnectRoute: AuthConnectRoute, AuthSetupRoute: AuthSetupRoute, AuthSignInRoute: AuthSignInRoute, AuthSignUpRoute: AuthSignUpRoute, UpgradeSuccessRoute: UpgradeSuccessRoute, + ApiAdminSetPlanRoute: ApiAdminSetPlanRoute, ApiBillingCheckoutRoute: ApiBillingCheckoutRoute, ApiBillingWebhookRoute: ApiBillingWebhookRoute, ApiGithubWebhookRoute: ApiGithubWebhookRoute, From 29ebd178cd068e72d1055cdd6bc4ef97174d2d0f Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Thu, 13 Aug 2026 19:57:40 +0100 Subject: [PATCH 21/26] chore(env): remove internal observability vars from the example OpenTelemetry/SigNoz config is personal operator tooling, not needed by contributors deploying Jargons; drop it from .env.example. ADMIN_USERNAMES is intentionally left undocumented here too. --- .env.example | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.env.example b/.env.example index f1f8f6d..7680b99 100644 --- a/.env.example +++ b/.env.example @@ -28,9 +28,3 @@ BACHS_API_KEY="your-bachs-secret-key" BACHS_API_BASE="https://sandbox-api.bachs.io" # live: https://api.bachs.io BACHS_PRO_PRODUCT_ID="your-bachs-pro-product-id" BACHS_WEBHOOK_SECRET="your-bachs-webhook-signing-secret" - -# --- OpenTelemetry -> SigNoz --- -# Point OTEL_EXPORTER_OTLP_ENDPOINT at self-hosted SigNoz (local Docker) or SigNoz Cloud. -OTEL_SERVICE_NAME="jargons-review-agent" -OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318" -OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf" From c5b1018c7fa3eb91edb20e588a8c6aca85cb0f8e Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Fri, 14 Aug 2026 09:32:17 +0100 Subject: [PATCH 22/26] feat(db): add users.is_admin for operator access Boolean flag (default false) granting /admin dashboard access. Set on a user's own row; the owner sets their own. --- src/db/schema.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/db/schema.ts b/src/db/schema.ts index 0d067d8..1d8540d 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -54,6 +54,9 @@ export const users = pgTable( name: text('name'), email: text('email'), avatarUrl: text('avatar_url'), + // Operator/admin access to the /admin dashboard. The owner is always an + // admin by GitHub id; this grants access to additional accounts. + isAdmin: boolean('is_admin').notNull().default(false), // Set the first time the user finishes or skips onboarding; null means the // guided onboarding has not been dismissed yet. onboardedAt: timestamp('onboarded_at', { withTimezone: true }), From 5031967dbc1be0eb8ed900bd35dfbe8689fa012f Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Fri, 14 Aug 2026 09:32:17 +0100 Subject: [PATCH 23/26] feat(db): migration for users.is_admin --- drizzle/0002_luxuriant_joseph.sql | 1 + drizzle/meta/0002_snapshot.json | 1064 +++++++++++++++++++++++++++++ drizzle/meta/_journal.json | 7 + 3 files changed, 1072 insertions(+) create mode 100644 drizzle/0002_luxuriant_joseph.sql create mode 100644 drizzle/meta/0002_snapshot.json diff --git a/drizzle/0002_luxuriant_joseph.sql b/drizzle/0002_luxuriant_joseph.sql new file mode 100644 index 0000000..d0e05d2 --- /dev/null +++ b/drizzle/0002_luxuriant_joseph.sql @@ -0,0 +1 @@ +ALTER TABLE "users" ADD COLUMN "is_admin" boolean DEFAULT false NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..37e62cc --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,1064 @@ +{ + "id": "aec736e3-cda3-493c-b052-c8ff300f235c", + "prevId": "1f839194-0176-4cd4-b818-c454479087bc", + "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 + }, + "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 + }, + "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 + }, + "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 + }, + "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 diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index fec878f..82dbdb4 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1786644826598, "tag": "0001_jazzy_darkstar", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1786695596743, + "tag": "0002_luxuriant_joseph", + "breakpoints": true } ] } \ No newline at end of file From b1b41ed10fbd63f518cdf2a86b98d97904d24471 Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Fri, 14 Aug 2026 09:32:18 +0100 Subject: [PATCH 24/26] feat(admin): gate access by the is_admin DB flag, not an env allowlist isAdmin() checks the users.is_admin flag on the signed-in account's own row, instead of the mutable-username ADMIN_USERNAMES env allowlist. The flag is tied to the account row (created from an immutable GitHub id), so renaming/reclaiming a username can't grant access. --- src/server/admin.ts | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/server/admin.ts b/src/server/admin.ts index 672421d..d6f65b6 100644 --- a/src/server/admin.ts +++ b/src/server/admin.ts @@ -1,39 +1,41 @@ // Operator/admin surface: platform-wide analytics and light user management. // This exposes EVERY workspace's data, so access is gated server-side on every -// call (never trust the UI). Admins are named in the ADMIN_USERNAMES env var -// (comma-separated GitHub usernames). +// call (never trust the UI). Admin access is granted via the users.is_admin +// flag on a user's own row โ€” never a mutable username or a hardcoded id. import { createServerFn } from '@tanstack/react-start' import { loadDb } from '../db/load' import { PRO_PRICE_USD } from '../lib/plans' -import { getOptionalEnv } from './env' import { getCurrentUserFromCookie } from './github-auth' import type { CurrentUser } from './github-auth' -export function isAdminUsername(username: string): boolean { - const allow = getOptionalEnv('ADMIN_USERNAMES', '') - .split(',') - .map((entry) => entry.trim().toLowerCase()) - .filter(Boolean) - return allow.includes(username.toLowerCase()) +/** True if the signed-in user has the is_admin flag set on their row. */ +export async function isAdmin(user: CurrentUser): Promise { + const { eq, db, users } = await loadDb() + const rows = await db + .select({ isAdmin: users.isAdmin }) + .from(users) + .where(eq(users.id, user.id)) + .limit(1) + return rows.length > 0 && rows[0].isAdmin } /** Throws unless the signed-in user is an admin. Returns the admin user. */ export async function requireAdmin(): Promise { const user = await getCurrentUserFromCookie() - if (!user || !isAdminUsername(user.username)) { + if (!user || !(await isAdmin(user))) { throw new Error('forbidden') } return user } // Used by the route's beforeLoad to gate access (returns null instead of -// throwing so the route can redirect cleanly). The env check stays server-side. +// throwing so the route can redirect cleanly). export const getAdminContext = createServerFn({ method: 'GET' }).handler( async (): Promise<{ username: string } | null> => { const user = await getCurrentUserFromCookie() - if (!user || !isAdminUsername(user.username)) { + if (!user || !(await isAdmin(user))) { return null } return { username: user.username } From eadb1876dc7fc5117ac435bca395d769a0c888ca Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Fri, 14 Aug 2026 09:32:18 +0100 Subject: [PATCH 25/26] feat(admin): use the DB isAdmin check in the plan-change route --- src/routes/api.admin.set-plan.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/api.admin.set-plan.tsx b/src/routes/api.admin.set-plan.tsx index d2a4e69..312e121 100644 --- a/src/routes/api.admin.set-plan.tsx +++ b/src/routes/api.admin.set-plan.tsx @@ -1,6 +1,6 @@ import { createFileRoute } from '@tanstack/react-router' -import { isAdminUsername, setWorkspacePlanAsAdmin } from '../server/admin' +import { isAdmin, setWorkspacePlanAsAdmin } from '../server/admin' import { getCurrentUserFromRequest } from '../server/github-auth' // Admin-only: manually set a workspace's plan (free โ†” pro). Re-checks the admin @@ -10,7 +10,7 @@ export const Route = createFileRoute('/api/admin/set-plan')({ handlers: { POST: async ({ request }) => { const user = await getCurrentUserFromRequest(request) - if (!user || !isAdminUsername(user.username)) { + if (!user || !(await isAdmin(user))) { return json({ error: 'forbidden' }, 403) } From 1eaf5761f9fe79af25409a26c9c4adecd17757ff Mon Sep 17 00:00:00 2001 From: devtofunmi Date: Fri, 14 Aug 2026 09:32:18 +0100 Subject: [PATCH 26/26] chore(env): drop the unused ADMIN_USERNAMES key --- src/server/env.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/server/env.ts b/src/server/env.ts index cb2e525..fecf91a 100644 --- a/src/server/env.ts +++ b/src/server/env.ts @@ -12,7 +12,6 @@ type RequiredEnvKey = type OptionalEnvKey = | 'APP_URL' | 'NODE_ENV' - | 'ADMIN_USERNAMES' | 'LLM_PROVIDER' | 'LLM_MODEL' | 'GEMINI_API_KEY'