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"
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
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/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
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 970edb1..82dbdb4 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -8,6 +8,20 @@
"when": 1786571980089,
"tag": "0000_small_mandarin",
"breakpoints": true
+ },
+ {
+ "idx": 1,
+ "version": "7",
+ "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
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}%
+
+
+ ))}
+
+ )}
+
+ )
+}
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)}
+
+
+ ))}
+
+ )}
+
+ )
+}
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 }) => (
+
+
+
+ {value}
+
+ {sub ? (
+ {sub}
+ ) : null}
+
+ ))}
+
+ )
+}
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]) => (
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+ )}
+
+ )
+}
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
+}
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})
+
+
+
+
+
+
+
+ | User |
+ Plan |
+ Runs |
+ Repos |
+ Reviews |
+ Scans |
+ Joined |
+ |
+
+
+
+ {users.map((user) => (
+
+
+
+ {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 (
+
+ )
+}
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 ? (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+ )}
+
+ )
+}
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
+}
diff --git a/src/db/schema.ts b/src/db/schema.ts
index b90ee21..1d8540d 100644
--- a/src/db/schema.ts
+++ b/src/db/schema.ts
@@ -1,5 +1,6 @@
import {
boolean,
+ index,
integer,
jsonb,
pgEnum,
@@ -53,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 }),
@@ -239,6 +243,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')
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)),
+ )
+}
diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts
index f033535..dcdd8f8 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'
import { Route as AppScansScanIdIndexRouteImport } from './routes/app.scans.$scanId.index'
import { Route as AppScansScanIdFindingIdRouteImport } from './routes/app.scans.$scanId.$findingId'
@@ -72,6 +75,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: '/',
@@ -127,6 +135,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: '/',
@@ -192,6 +205,11 @@ 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)
const AppScansScanIdIndexRoute = AppScansScanIdIndexRouteImport.update({
id: '/',
path: '/',
@@ -205,12 +223,14 @@ const AppScansScanIdFindingIdRoute = AppScansScanIdFindingIdRouteImport.update({
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
@@ -221,6 +241,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
@@ -239,11 +260,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
@@ -251,6 +274,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
@@ -269,12 +293,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
@@ -285,6 +311,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
@@ -305,12 +332,14 @@ export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
+ | '/admin'
| '/app'
| '/docs'
| '/onboarding'
| '/pricing'
| '/privacy'
| '/terms'
+ | '/api/track'
| '/app/repositories'
| '/app/reviews'
| '/app/scans'
@@ -321,6 +350,7 @@ export interface FileRouteTypes {
| '/auth/sign-up'
| '/upgrade/success'
| '/app/'
+ | '/api/admin/set-plan'
| '/api/billing/checkout'
| '/api/billing/webhook'
| '/api/github/webhook'
@@ -339,11 +369,13 @@ export interface FileRouteTypes {
fileRoutesByTo: FileRoutesByTo
to:
| '/'
+ | '/admin'
| '/docs'
| '/onboarding'
| '/pricing'
| '/privacy'
| '/terms'
+ | '/api/track'
| '/app/settings'
| '/auth/connect'
| '/auth/setup'
@@ -351,6 +383,7 @@ export interface FileRouteTypes {
| '/auth/sign-up'
| '/upgrade/success'
| '/app'
+ | '/api/admin/set-plan'
| '/api/billing/checkout'
| '/api/billing/webhook'
| '/api/github/webhook'
@@ -368,12 +401,14 @@ export interface FileRouteTypes {
id:
| '__root__'
| '/'
+ | '/admin'
| '/app'
| '/docs'
| '/onboarding'
| '/pricing'
| '/privacy'
| '/terms'
+ | '/api/track'
| '/app/repositories'
| '/app/reviews'
| '/app/scans'
@@ -384,6 +419,7 @@ export interface FileRouteTypes {
| '/auth/sign-up'
| '/upgrade/success'
| '/app/'
+ | '/api/admin/set-plan'
| '/api/billing/checkout'
| '/api/billing/webhook'
| '/api/github/webhook'
@@ -403,17 +439,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
@@ -467,6 +506,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: '/'
@@ -544,6 +590,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: '/'
@@ -635,6 +688,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
+ }
'/app/scans/$scanId/': {
id: '/app/scans/$scanId/'
path: '/'
@@ -728,17 +788,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,
@@ -750,12 +813,3 @@ const rootRouteChildren: RootRouteChildren = {
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes()
-
-import type { getRouter } from './router.tsx'
-import type { createStart } from '@tanstack/react-start'
-declare module '@tanstack/react-start' {
- interface Register {
- ssr: true
- router: Awaited>
- }
-}
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
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/routes/api.admin.set-plan.tsx b/src/routes/api.admin.set-plan.tsx
new file mode 100644
index 0000000..312e121
--- /dev/null
+++ b/src/routes/api.admin.set-plan.tsx
@@ -0,0 +1,50 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+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
+// 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 || !(await isAdmin(user))) {
+ 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' },
+ })
+}
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 })
+ },
+ },
+ },
+})
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 (
+
diff --git a/src/server/admin.ts b/src/server/admin.ts
new file mode 100644
index 0000000..d6f65b6
--- /dev/null
+++ b/src/server/admin.ts
@@ -0,0 +1,258 @@
+// 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). 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 { getCurrentUserFromCookie } from './github-auth'
+import type { CurrentUser } from './github-auth'
+
+/** 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 || !(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).
+export const getAdminContext = createServerFn({ method: 'GET' }).handler(
+ async (): Promise<{ username: string } | null> => {
+ const user = await getCurrentUserFromCookie()
+ if (!user || !(await isAdmin(user))) {
+ 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)
+ }
+}