diff --git a/drizzle/0017_omniscient_dagger.sql b/drizzle/0017_omniscient_dagger.sql new file mode 100644 index 0000000..0937654 --- /dev/null +++ b/drizzle/0017_omniscient_dagger.sql @@ -0,0 +1,23 @@ +CREATE TABLE `reddit_attributions` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `organization_id` text NOT NULL, + `click_id` text, + `uuid` text, + `landing_page` text, + `referrer` text, + `utm_source` text, + `utm_medium` text, + `utm_campaign` text, + `utm_term` text, + `utm_content` text, + `signup_sent_at` text, + `purchase_sent_at` text, + `created_at` text DEFAULT (current_timestamp) NOT NULL, + `updated_at` text DEFAULT (current_timestamp) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`organization_id`) REFERENCES `organization`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `reddit_attributions_user_idx` ON `reddit_attributions` (`user_id`);--> statement-breakpoint +CREATE INDEX `reddit_attributions_organization_idx` ON `reddit_attributions` (`organization_id`); \ No newline at end of file diff --git a/drizzle/meta/0017_snapshot.json b/drizzle/meta/0017_snapshot.json new file mode 100644 index 0000000..9ff5e40 --- /dev/null +++ b/drizzle/meta/0017_snapshot.json @@ -0,0 +1,2333 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "e35850a7-60a5-4c9a-a8f5-875269e4b6da", + "prevId": "03172742-ed12-4540-a6d2-89622d4f3a49", + "tables": { + "audit_lighthouse_results": { + "name": "audit_lighthouse_results", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "audit_id": { + "name": "audit_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "page_id": { + "name": "page_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "strategy": { + "name": "strategy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "performance_score": { + "name": "performance_score", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accessibility_score": { + "name": "accessibility_score", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "best_practices_score": { + "name": "best_practices_score", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seo_score": { + "name": "seo_score", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lcp_ms": { + "name": "lcp_ms", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cls": { + "name": "cls", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inp_ms": { + "name": "inp_ms", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ttfb_ms": { + "name": "ttfb_ms", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "r2_key": { + "name": "r2_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_size_bytes": { + "name": "payload_size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "audit_lighthouse_results_audit_id_idx": { + "name": "audit_lighthouse_results_audit_id_idx", + "columns": [ + "audit_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_lighthouse_results_audit_id_audits_id_fk": { + "name": "audit_lighthouse_results_audit_id_audits_id_fk", + "tableFrom": "audit_lighthouse_results", + "tableTo": "audits", + "columnsFrom": [ + "audit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_lighthouse_results_page_id_audit_pages_id_fk": { + "name": "audit_lighthouse_results_page_id_audit_pages_id_fk", + "tableFrom": "audit_lighthouse_results", + "tableTo": "audit_pages", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_pages": { + "name": "audit_pages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "audit_id": { + "name": "audit_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "redirect_url": { + "name": "redirect_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "meta_description": { + "name": "meta_description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "canonical_url": { + "name": "canonical_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "robots_meta": { + "name": "robots_meta", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "og_title": { + "name": "og_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "og_description": { + "name": "og_description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "og_image": { + "name": "og_image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "h1_count": { + "name": "h1_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "h2_count": { + "name": "h2_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "h3_count": { + "name": "h3_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "h4_count": { + "name": "h4_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "h5_count": { + "name": "h5_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "h6_count": { + "name": "h6_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "heading_order_json": { + "name": "heading_order_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "word_count": { + "name": "word_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "images_total": { + "name": "images_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "images_missing_alt": { + "name": "images_missing_alt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "images_json": { + "name": "images_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "internal_link_count": { + "name": "internal_link_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "external_link_count": { + "name": "external_link_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "has_structured_data": { + "name": "has_structured_data", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "hreflang_tags_json": { + "name": "hreflang_tags_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_indexable": { + "name": "is_indexable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "response_time_ms": { + "name": "response_time_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "audit_pages_audit_id_idx": { + "name": "audit_pages_audit_id_idx", + "columns": [ + "audit_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_pages_audit_id_audits_id_fk": { + "name": "audit_pages_audit_id_audits_id_fk", + "tableFrom": "audit_pages", + "tableTo": "audits", + "columnsFrom": [ + "audit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audits": { + "name": "audits", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_by_user_id": { + "name": "started_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_url": { + "name": "start_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'running'" + }, + "workflow_instance_id": { + "name": "workflow_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "pages_crawled": { + "name": "pages_crawled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "pages_total": { + "name": "pages_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "lighthouse_total": { + "name": "lighthouse_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "lighthouse_completed": { + "name": "lighthouse_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "lighthouse_failed": { + "name": "lighthouse_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "current_phase": { + "name": "current_phase", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'discovery'" + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "audits_project_id_idx": { + "name": "audits_project_id_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "audits_started_by_user_id_idx": { + "name": "audits_started_by_user_id_idx", + "columns": [ + "started_by_user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audits_project_id_projects_id_fk": { + "name": "audits_project_id_projects_id_fk", + "tableFrom": "audits", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "delegated_users": { + "name": "delegated_users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "delegated_users_email_unique": { + "name": "delegated_users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "keyword_metrics": { + "name": "keyword_metrics", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "location_code": { + "name": "location_code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "language_code": { + "name": "language_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'en'" + }, + "search_volume": { + "name": "search_volume", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpc": { + "name": "cpc", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "competition": { + "name": "competition", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "keyword_difficulty": { + "name": "keyword_difficulty", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "intent": { + "name": "intent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "monthly_searches": { + "name": "monthly_searches", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fetched_at": { + "name": "fetched_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "keyword_metrics_unique_project_keyword_location_language": { + "name": "keyword_metrics_unique_project_keyword_location_language", + "columns": [ + "project_id", + "keyword", + "location_code", + "language_code" + ], + "isUnique": true + }, + "keyword_metrics_lookup_idx": { + "name": "keyword_metrics_lookup_idx", + "columns": [ + "project_id", + "keyword", + "location_code", + "language_code", + "fetched_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "keyword_metrics_project_id_projects_id_fk": { + "name": "keyword_metrics_project_id_projects_id_fk", + "tableFrom": "keyword_metrics", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "projects_one_default_per_organization_idx": { + "name": "projects_one_default_per_organization_idx", + "columns": [ + "organization_id" + ], + "isUnique": true, + "where": "\"projects\".\"name\" = 'Default' AND \"projects\".\"domain\" IS NULL" + } + }, + "foreignKeys": { + "projects_organization_id_organization_id_fk": { + "name": "projects_organization_id_organization_id_fk", + "tableFrom": "projects", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rank_check_runs": { + "name": "rank_check_runs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "keywords_total": { + "name": "keywords_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "keywords_checked": { + "name": "keywords_checked", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "is_subset_run": { + "name": "is_subset_run", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "rank_check_runs_config_idx": { + "name": "rank_check_runs_config_idx", + "columns": [ + "config_id", + "started_at" + ], + "isUnique": false + }, + "rank_check_runs_project_idx": { + "name": "rank_check_runs_project_idx", + "columns": [ + "project_id", + "started_at" + ], + "isUnique": false + }, + "rank_check_runs_one_active_per_config_idx": { + "name": "rank_check_runs_one_active_per_config_idx", + "columns": [ + "config_id" + ], + "isUnique": true, + "where": "\"rank_check_runs\".\"status\" IN ('pending', 'running')" + } + }, + "foreignKeys": { + "rank_check_runs_config_id_rank_tracking_configs_id_fk": { + "name": "rank_check_runs_config_id_rank_tracking_configs_id_fk", + "tableFrom": "rank_check_runs", + "tableTo": "rank_tracking_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rank_check_runs_project_id_projects_id_fk": { + "name": "rank_check_runs_project_id_projects_id_fk", + "tableFrom": "rank_check_runs", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rank_snapshots": { + "name": "rank_snapshots", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tracking_keyword_id": { + "name": "tracking_keyword_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device": { + "name": "device", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "serp_features": { + "name": "serp_features", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "checked_at": { + "name": "checked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "rank_snapshots_run_idx": { + "name": "rank_snapshots_run_idx", + "columns": [ + "run_id" + ], + "isUnique": false + }, + "rank_snapshots_keyword_device_idx": { + "name": "rank_snapshots_keyword_device_idx", + "columns": [ + "tracking_keyword_id", + "device", + "checked_at" + ], + "isUnique": false + }, + "rank_snapshots_run_keyword_device_idx": { + "name": "rank_snapshots_run_keyword_device_idx", + "columns": [ + "run_id", + "tracking_keyword_id", + "device" + ], + "isUnique": true + } + }, + "foreignKeys": { + "rank_snapshots_run_id_rank_check_runs_id_fk": { + "name": "rank_snapshots_run_id_rank_check_runs_id_fk", + "tableFrom": "rank_snapshots", + "tableTo": "rank_check_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rank_tracking_configs": { + "name": "rank_tracking_configs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "location_code": { + "name": "location_code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2840 + }, + "language_code": { + "name": "language_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'en'" + }, + "devices": { + "name": "devices", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'both'" + }, + "serp_depth": { + "name": "serp_depth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schedule_interval": { + "name": "schedule_interval", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'weekly'" + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_check_at": { + "name": "next_check_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_skip_reason": { + "name": "last_skip_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "rank_tracking_configs_project_domain_location_idx": { + "name": "rank_tracking_configs_project_domain_location_idx", + "columns": [ + "project_id", + "domain", + "location_code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "rank_tracking_configs_project_id_projects_id_fk": { + "name": "rank_tracking_configs_project_id_projects_id_fk", + "tableFrom": "rank_tracking_configs", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rank_tracking_keywords": { + "name": "rank_tracking_keywords", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "search_volume": { + "name": "search_volume", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "keyword_difficulty": { + "name": "keyword_difficulty", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpc": { + "name": "cpc", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metrics_fetched_at": { + "name": "metrics_fetched_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "rank_tracking_keywords_config_keyword_idx": { + "name": "rank_tracking_keywords_config_keyword_idx", + "columns": [ + "config_id", + "keyword" + ], + "isUnique": true + } + }, + "foreignKeys": { + "rank_tracking_keywords_config_id_rank_tracking_configs_id_fk": { + "name": "rank_tracking_keywords_config_id_rank_tracking_configs_id_fk", + "tableFrom": "rank_tracking_keywords", + "tableTo": "rank_tracking_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saved_keyword_tag_assignments": { + "name": "saved_keyword_tag_assignments", + "columns": { + "saved_keyword_id": { + "name": "saved_keyword_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "saved_keyword_tag_assignments_unique_idx": { + "name": "saved_keyword_tag_assignments_unique_idx", + "columns": [ + "saved_keyword_id", + "tag_id" + ], + "isUnique": true + }, + "saved_keyword_tag_assignments_keyword_idx": { + "name": "saved_keyword_tag_assignments_keyword_idx", + "columns": [ + "saved_keyword_id" + ], + "isUnique": false + }, + "saved_keyword_tag_assignments_tag_idx": { + "name": "saved_keyword_tag_assignments_tag_idx", + "columns": [ + "tag_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saved_keyword_tag_assignments_saved_keyword_id_saved_keywords_id_fk": { + "name": "saved_keyword_tag_assignments_saved_keyword_id_saved_keywords_id_fk", + "tableFrom": "saved_keyword_tag_assignments", + "tableTo": "saved_keywords", + "columnsFrom": [ + "saved_keyword_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saved_keyword_tag_assignments_tag_id_saved_keyword_tags_id_fk": { + "name": "saved_keyword_tag_assignments_tag_id_saved_keyword_tags_id_fk", + "tableFrom": "saved_keyword_tag_assignments", + "tableTo": "saved_keyword_tags", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saved_keyword_tags": { + "name": "saved_keyword_tags", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "normalized_name": { + "name": "normalized_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "saved_keyword_tags_project_normalized_name_idx": { + "name": "saved_keyword_tags_project_normalized_name_idx", + "columns": [ + "project_id", + "normalized_name" + ], + "isUnique": true + }, + "saved_keyword_tags_project_name_idx": { + "name": "saved_keyword_tags_project_name_idx", + "columns": [ + "project_id", + "name" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saved_keyword_tags_project_id_projects_id_fk": { + "name": "saved_keyword_tags_project_id_projects_id_fk", + "tableFrom": "saved_keyword_tags", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saved_keywords": { + "name": "saved_keywords", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "location_code": { + "name": "location_code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2840 + }, + "language_code": { + "name": "language_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'en'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "saved_keywords_unique_project_keyword_location_language": { + "name": "saved_keywords_unique_project_keyword_location_language", + "columns": [ + "project_id", + "keyword", + "location_code", + "language_code" + ], + "isUnique": true + }, + "saved_keywords_project_created_idx": { + "name": "saved_keywords_project_created_idx", + "columns": [ + "project_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saved_keywords_project_id_projects_id_fk": { + "name": "saved_keywords_project_id_projects_id_fk", + "tableFrom": "saved_keywords", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invitation": { + "name": "invitation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "member": { + "name": "member", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization": { + "name": "organization", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + }, + "organization_slug_uidx": { + "name": "organization_slug_uidx", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "analytics_opted_out": { + "name": "analytics_opted_out", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + "identifier" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reddit_attributions": { + "name": "reddit_attributions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "click_id": { + "name": "click_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "landing_page": { + "name": "landing_page", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "referrer": { + "name": "referrer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "utm_source": { + "name": "utm_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "utm_medium": { + "name": "utm_medium", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "utm_campaign": { + "name": "utm_campaign", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "utm_term": { + "name": "utm_term", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "utm_content": { + "name": "utm_content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signup_sent_at": { + "name": "signup_sent_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "purchase_sent_at": { + "name": "purchase_sent_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "reddit_attributions_user_idx": { + "name": "reddit_attributions_user_idx", + "columns": [ + "user_id" + ], + "isUnique": true + }, + "reddit_attributions_organization_idx": { + "name": "reddit_attributions_organization_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reddit_attributions_user_id_user_id_fk": { + "name": "reddit_attributions_user_id_user_id_fk", + "tableFrom": "reddit_attributions", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reddit_attributions_organization_id_organization_id_fk": { + "name": "reddit_attributions_organization_id_organization_id_fk", + "tableFrom": "reddit_attributions", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index a922d10..2b7fab9 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -120,6 +120,13 @@ "when": 1779138211145, "tag": "0016_magical_the_fallen", "breakpoints": true + }, + { + "idx": 17, + "version": "6", + "when": 1779331224828, + "tag": "0017_omniscient_dagger", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/src/client/lib/reddit-attribution.ts b/src/client/lib/reddit-attribution.ts new file mode 100644 index 0000000..c9050e6 --- /dev/null +++ b/src/client/lib/reddit-attribution.ts @@ -0,0 +1,83 @@ +import type { RedditAttributionInput } from "@/shared/reddit-attribution"; +import { redditAttributionSchema } from "@/shared/reddit-attribution"; + +const STORAGE_KEY = "openseo:reddit-attribution"; +const SIGNUP_SENT_KEY = "openseo:reddit-signup-conversion-sent"; + +function readCookie(name: string) { + const prefix = `${name}=`; + return document.cookie + .split(";") + .map((part) => part.trim()) + .find((part) => part.startsWith(prefix)) + ?.slice(prefix.length); +} + +function firstSearchValue(searchParams: URLSearchParams, names: string[]) { + for (const name of names) { + const value = searchParams.get(name)?.trim(); + if (value) return value; + } + return undefined; +} + +export function captureRedditAttributionFromLocation() { + if (typeof window === "undefined") return; + + const url = new URL(window.location.href); + const current = getStoredRedditAttribution(); + const next = redditAttributionSchema.parse({ + clickId: + firstSearchValue(url.searchParams, ["rdt_cid", "reddit_click_id"]) ?? + current?.clickId, + uuid: readCookie("_rdt_uuid") ?? current?.uuid, + landingPage: current?.landingPage ?? url.toString(), + referrer: (current?.referrer ?? document.referrer) || undefined, + utmSource: + firstSearchValue(url.searchParams, ["utm_source"]) ?? current?.utmSource, + utmMedium: + firstSearchValue(url.searchParams, ["utm_medium"]) ?? current?.utmMedium, + utmCampaign: + firstSearchValue(url.searchParams, ["utm_campaign"]) ?? + current?.utmCampaign, + utmTerm: + firstSearchValue(url.searchParams, ["utm_term"]) ?? current?.utmTerm, + utmContent: + firstSearchValue(url.searchParams, ["utm_content"]) ?? + current?.utmContent, + }); + + if (!next.clickId && next.utmSource?.toLowerCase() !== "reddit") return; + + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); +} + +export function getStoredRedditAttribution(): RedditAttributionInput | null { + if (typeof window === "undefined") return null; + + const raw = window.localStorage.getItem(STORAGE_KEY); + if (!raw) return null; + + try { + const parsed: unknown = JSON.parse(raw); + const result = redditAttributionSchema.safeParse(parsed); + return result.success ? result.data : null; + } catch { + return null; + } +} + +export function hasMarkedRedditSignupConversion(userId: string) { + if (typeof window === "undefined") return true; + return window.localStorage.getItem(SIGNUP_SENT_KEY) === userId; +} + +export function markRedditSignupConversion(userId: string) { + if (typeof window === "undefined") return; + window.localStorage.setItem(SIGNUP_SENT_KEY, userId); +} + +export function unmarkRedditSignupConversion() { + if (typeof window === "undefined") return; + window.localStorage.removeItem(SIGNUP_SENT_KEY); +} diff --git a/src/db/reddit-attribution.schema.ts b/src/db/reddit-attribution.schema.ts new file mode 100644 index 0000000..7b1f9c5 --- /dev/null +++ b/src/db/reddit-attribution.schema.ts @@ -0,0 +1,37 @@ +import { sql } from "drizzle-orm"; +import { index, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"; +import { organization, user } from "./better-auth-schema"; + +export const redditAttributions = sqliteTable( + "reddit_attributions", + { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + organizationId: text("organization_id") + .notNull() + .references(() => organization.id, { onDelete: "cascade" }), + clickId: text("click_id"), + uuid: text("uuid"), + landingPage: text("landing_page"), + referrer: text("referrer"), + utmSource: text("utm_source"), + utmMedium: text("utm_medium"), + utmCampaign: text("utm_campaign"), + utmTerm: text("utm_term"), + utmContent: text("utm_content"), + signupSentAt: text("signup_sent_at"), + purchaseSentAt: text("purchase_sent_at"), + createdAt: text("created_at") + .notNull() + .default(sql`(current_timestamp)`), + updatedAt: text("updated_at") + .notNull() + .default(sql`(current_timestamp)`), + }, + (table) => [ + uniqueIndex("reddit_attributions_user_idx").on(table.userId), + index("reddit_attributions_organization_idx").on(table.organizationId), + ], +); diff --git a/src/db/schema.ts b/src/db/schema.ts index 789dbcd..1dc1f9b 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,2 +1,3 @@ export * from "./app.schema"; export * from "./better-auth-schema"; +export * from "./reddit-attribution.schema"; diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 1e7999d..224fea5 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -19,6 +19,13 @@ import { startAnalyticsCapture, stopAnalyticsCapture, } from "@/client/lib/posthog"; +import { + captureRedditAttributionFromLocation, + getStoredRedditAttribution, + hasMarkedRedditSignupConversion, + markRedditSignupConversion, + unmarkRedditSignupConversion, +} from "@/client/lib/reddit-attribution"; import { NotFound } from "@/client/components/NotFound"; import appCss from "@/client/styles/app.css?url"; import { useSession } from "@/lib/auth-client"; @@ -26,6 +33,7 @@ import { isHostedClientAuthMode } from "@/lib/auth-mode"; import { Toaster } from "sonner"; import { queryClient } from "@/client/tanstack-db"; import { getActiveOrganizationId } from "@/lib/auth-session"; +import { captureRedditConversionEvent } from "@/serverFunctions/redditConversions"; export const Route = createRootRoute({ head: () => ({ @@ -90,6 +98,11 @@ function PostHogBootstrap() { const optedOut = session?.user?.analyticsOptedOut === true; const organizationId = getActiveOrganizationId(session); const previousUserIdRef = React.useRef(null); + const redditSignupInFlightRef = React.useRef(false); + + React.useEffect(() => { + captureRedditAttributionFromLocation(); + }, []); React.useEffect(() => { if (!isHostedMode || isSessionPending) { @@ -108,6 +121,32 @@ function PostHogBootstrap() { } }, [isHostedMode, isSessionPending, optedOut, organizationId, userId]); + React.useEffect(() => { + if (!isHostedMode || isSessionPending || !userId) return; + if (hasMarkedRedditSignupConversion(userId)) return; + + const attribution = getStoredRedditAttribution(); + if (!attribution) return; + if (redditSignupInFlightRef.current) return; + + redditSignupInFlightRef.current = true; + void captureRedditConversionEvent({ + data: { attribution, eventType: "SignUp" }, + }) + .then((result) => { + if (result.status === "sent" || result.status === "already_sent") { + markRedditSignupConversion(userId); + } + }) + .catch(() => { + // The server deduplicates this event; allow a future session to retry. + unmarkRedditSignupConversion(); + }) + .finally(() => { + redditSignupInFlightRef.current = false; + }); + }, [isHostedMode, isSessionPending, userId]); + return null; } diff --git a/src/routes/_app/billing.tsx b/src/routes/_app/billing.tsx index 2e3a213..af4891c 100644 --- a/src/routes/_app/billing.tsx +++ b/src/routes/_app/billing.tsx @@ -1,9 +1,10 @@ import { createFileRoute, notFound } from "@tanstack/react-router"; import { AutumnProvider, useCustomer } from "autumn-js/react"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useSession } from "@/lib/auth-client"; import { isHostedClientAuthMode } from "@/lib/auth-mode"; import { getStandardErrorMessage } from "@/client/lib/error-messages"; +import { getStoredRedditAttribution } from "@/client/lib/reddit-attribution"; import { BillingUsageChart } from "@/client/features/billing/BillingUsageChart"; import { parseTopUpAmount } from "@/client/features/billing/HostedBillingContentUtils"; import { getBillingRouteState } from "@/client/features/billing/route-state"; @@ -17,6 +18,7 @@ import { AUTUMN_SEO_DATA_TOPUP_BALANCE_FEATURE_ID, autumnSeoDataCreditsToUsd, } from "@/shared/billing"; +import { captureRedditConversionEvent } from "@/serverFunctions/redditConversions"; export const Route = createFileRoute("/_app/billing")({ beforeLoad: () => { @@ -68,6 +70,20 @@ function BillingPageContent() { const { isValid: isValidTopUp, parsed: parsedTopUpAmount } = parseTopUpAmount(topUpAmount); + const checkoutCompleted = + typeof window !== "undefined" && + new URLSearchParams(window.location.search).get("checkout") === "success"; + + useEffect(() => { + if (!checkoutCompleted || billingRouteState !== "ready") return; + + const attribution = getStoredRedditAttribution(); + if (!attribution) return; + + void captureRedditConversionEvent({ + data: { attribution, eventType: "Purchase" }, + }); + }, [billingRouteState, checkoutCompleted]); if (billingRouteState === "loading") { return null; @@ -202,7 +218,7 @@ function BillingPageContent() { customerQuery.attach({ planId: AUTUMN_PAID_PLAN_ID, redirectMode: "always", - successUrl: window.location.href, + successUrl: `${window.location.origin}${window.location.pathname}?checkout=success`, }), "We couldn't start the checkout. Please try again.", ) diff --git a/src/routes/_authenticated.subscribe.tsx b/src/routes/_authenticated.subscribe.tsx index 89ae1fc..599eda3 100644 --- a/src/routes/_authenticated.subscribe.tsx +++ b/src/routes/_authenticated.subscribe.tsx @@ -4,11 +4,13 @@ import { useEffect, useState } from "react"; import { ArrowRight, Settings, User } from "lucide-react"; import { ThemePreferenceMenuItems } from "@/client/components/ThemePreferenceMenuItems"; import { captureClientEvent } from "@/client/lib/posthog"; +import { getStoredRedditAttribution } from "@/client/lib/reddit-attribution"; import { signOutAndRedirect, useSession } from "@/lib/auth-client"; import { getStandardErrorMessage } from "@/client/lib/error-messages"; import { getSubscribeRouteState } from "@/client/features/billing/route-state"; import { getCustomerPlanStatus } from "@/client/features/billing/plan-detection"; import { AUTUMN_PAID_PLAN_ID } from "@/shared/billing"; +import { captureRedditConversionEvent } from "@/serverFunctions/redditConversions"; export const Route = createFileRoute("/_authenticated/subscribe")({ validateSearch: (search: Record) => ({ @@ -54,6 +56,15 @@ function SubscribePageContent() { if (subscribeRouteState === "redirectToApp") { if (checkoutCompleted) { captureClientEvent("billing:checkout_success"); + const attribution = getStoredRedditAttribution(); + if (attribution) { + void captureRedditConversionEvent({ + data: { attribution, eventType: "Purchase" }, + }).finally(() => { + void navigate({ to: "/", replace: true }); + }); + return; + } } void navigate({ to: "/", replace: true }); } diff --git a/src/server/lib/reddit-conversions.ts b/src/server/lib/reddit-conversions.ts new file mode 100644 index 0000000..354f076 --- /dev/null +++ b/src/server/lib/reddit-conversions.ts @@ -0,0 +1,172 @@ +import { env } from "cloudflare:workers"; +import { eq } from "drizzle-orm"; +import { db } from "@/db"; +import { redditAttributions } from "@/db/schema"; +import { + hasRedditAttribution, + type RedditAttributionInput, +} from "@/shared/reddit-attribution"; + +type RedditConversionType = "SignUp" | "Purchase"; + +type CaptureRedditConversionArgs = { + attribution: RedditAttributionInput; + conversionId: string; + email: string; + eventType: RedditConversionType; + organizationId: string; + userId: string; + valueDecimal?: number; + currency?: string; +}; + +function getEnv(name: string) { + const value: unknown = Reflect.get(env, name); + return typeof value === "string" ? value.trim() : ""; +} + +async function sha256(value: string) { + const bytes = new TextEncoder().encode(value.trim().toLowerCase()); + const hash = await crypto.subtle.digest("SHA-256", bytes); + return [...new Uint8Array(hash)] + .map((byte) => byte.toString(16).padStart(2, "0")) + .join(""); +} + +function getRedditConfig() { + const accountId = getEnv("REDDIT_AD_ACCOUNT_ID") || getEnv("REDDIT_PIXEL_ID"); + const accessToken = getEnv("REDDIT_CONVERSIONS_ACCESS_TOKEN"); + + if (!accountId || !accessToken) return null; + + return { accountId, accessToken }; +} + +async function upsertAttribution(args: CaptureRedditConversionArgs) { + const existing = await db.query.redditAttributions.findFirst({ + where: eq(redditAttributions.userId, args.userId), + }); + const now = new Date().toISOString(); + + if (existing) { + await db + .update(redditAttributions) + .set({ + clickId: existing.clickId ?? args.attribution.clickId, + uuid: existing.uuid ?? args.attribution.uuid, + landingPage: existing.landingPage ?? args.attribution.landingPage, + referrer: existing.referrer ?? args.attribution.referrer, + utmSource: existing.utmSource ?? args.attribution.utmSource, + utmMedium: existing.utmMedium ?? args.attribution.utmMedium, + utmCampaign: existing.utmCampaign ?? args.attribution.utmCampaign, + utmTerm: existing.utmTerm ?? args.attribution.utmTerm, + utmContent: existing.utmContent ?? args.attribution.utmContent, + updatedAt: now, + }) + .where(eq(redditAttributions.userId, args.userId)); + } else { + await db.insert(redditAttributions).values({ + id: crypto.randomUUID(), + userId: args.userId, + organizationId: args.organizationId, + clickId: args.attribution.clickId, + uuid: args.attribution.uuid, + landingPage: args.attribution.landingPage, + referrer: args.attribution.referrer, + utmSource: args.attribution.utmSource, + utmMedium: args.attribution.utmMedium, + utmCampaign: args.attribution.utmCampaign, + utmTerm: args.attribution.utmTerm, + utmContent: args.attribution.utmContent, + createdAt: now, + updatedAt: now, + }); + } +} + +async function hasSentConversion(args: CaptureRedditConversionArgs) { + const existing = await db.query.redditAttributions.findFirst({ + where: eq(redditAttributions.userId, args.userId), + }); + return args.eventType === "SignUp" + ? Boolean(existing?.signupSentAt) + : Boolean(existing?.purchaseSentAt); +} + +async function markConversionSent(args: CaptureRedditConversionArgs) { + const now = new Date().toISOString(); + const sentColumn = + args.eventType === "SignUp" ? "signupSentAt" : "purchaseSentAt"; + + await db + .update(redditAttributions) + .set({ + [sentColumn]: now, + updatedAt: now, + }) + .where(eq(redditAttributions.userId, args.userId)); +} + +export async function captureRedditConversion( + args: CaptureRedditConversionArgs, +) { + if (!hasRedditAttribution(args.attribution)) return "skipped" as const; + + await upsertAttribution(args); + if (await hasSentConversion(args)) return "already_sent" as const; + + const config = getRedditConfig(); + if (!config) return "stored" as const; + + const eventMetadata: Record = { + conversion_id: args.conversionId, + transaction_id: args.conversionId, + }; + if (args.valueDecimal !== undefined) { + eventMetadata.value_decimal = args.valueDecimal; + eventMetadata.currency = args.currency ?? "USD"; + eventMetadata.item_count = 1; + } + + const payload = { + events: [ + { + click_id: args.attribution.clickId, + event_at: new Date().toISOString(), + event_type: { + tracking_type: args.eventType, + }, + event_metadata: eventMetadata, + user: { + email: await sha256(args.email), + external_id: await sha256(args.userId), + uuid: args.attribution.uuid, + }, + }, + ], + }; + + const response = await fetch( + `https://ads-api.reddit.com/api/v2.0/conversions/events/${config.accountId}`, + { + method: "POST", + headers: { + Authorization: `Bearer ${config.accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }, + ); + + if (!response.ok) { + console.error("reddit conversion capture failed", { + status: response.status, + eventType: args.eventType, + userId: args.userId, + }); + return "failed" as const; + } + + await markConversionSent(args); + return "sent" as const; +} diff --git a/src/serverFunctions/redditConversions.ts b/src/serverFunctions/redditConversions.ts new file mode 100644 index 0000000..598e55b --- /dev/null +++ b/src/serverFunctions/redditConversions.ts @@ -0,0 +1,28 @@ +import { createServerFn } from "@tanstack/react-start"; +import { z } from "zod"; +import { captureRedditConversion } from "@/server/lib/reddit-conversions"; +import { redditAttributionSchema } from "@/shared/reddit-attribution"; +import { requireAuthenticatedContext } from "@/serverFunctions/middleware"; + +const conversionInputSchema = z.object({ + attribution: redditAttributionSchema, + eventType: z.enum(["SignUp", "Purchase"]), +}); + +export const captureRedditConversionEvent = createServerFn({ method: "POST" }) + .middleware(requireAuthenticatedContext) + .inputValidator((data: unknown) => conversionInputSchema.parse(data)) + .handler(async ({ data, context }) => { + const status = await captureRedditConversion({ + attribution: data.attribution, + conversionId: `${data.eventType.toLowerCase()}:${context.userId}`, + email: context.userEmail, + eventType: data.eventType, + organizationId: context.organizationId, + userId: context.userId, + valueDecimal: data.eventType === "Purchase" ? 10 : undefined, + currency: data.eventType === "Purchase" ? "USD" : undefined, + }); + + return { status }; + }); diff --git a/src/shared/reddit-attribution.ts b/src/shared/reddit-attribution.ts new file mode 100644 index 0000000..e4af62e --- /dev/null +++ b/src/shared/reddit-attribution.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; + +export const redditAttributionSchema = z.object({ + clickId: z.string().trim().min(1).max(256).optional(), + uuid: z.string().trim().min(1).max(256).optional(), + landingPage: z.string().trim().min(1).max(2048).optional(), + referrer: z.string().trim().max(2048).optional(), + utmSource: z.string().trim().min(1).max(256).optional(), + utmMedium: z.string().trim().min(1).max(256).optional(), + utmCampaign: z.string().trim().min(1).max(256).optional(), + utmTerm: z.string().trim().min(1).max(256).optional(), + utmContent: z.string().trim().min(1).max(256).optional(), +}); + +export type RedditAttributionInput = z.infer; + +export function hasRedditAttribution(input: RedditAttributionInput) { + return Boolean( + input.clickId || + input.uuid || + input.utmSource?.toLowerCase() === "reddit" || + input.referrer?.toLowerCase().includes("reddit."), + ); +} diff --git a/web/content/legal/privacy.md b/web/content/legal/privacy.md index ea0731e..cd87e83 100644 --- a/web/content/legal/privacy.md +++ b/web/content/legal/privacy.md @@ -33,7 +33,7 @@ The Site and the Service are not directed to anyone under the age of 13\. As sta II. HOW WE USE AND SHARE INFORMATION _Personal Information:_ -Except as otherwise stated in this Privacy Policy, we do not sell, trade, rent or otherwise share for marketing purposes your Personal Information with third parties without your consent. We do share Personal Information with vendors who are performing services for the Company, such as providers that help us send email communications, operate analytics, process billing, provide data requested through the Service, and host or support the infrastructure behind the Site and Service. These vendors may include Loops for email communications, Plausible for public-site analytics, PostHog for hosted analytics, session replay, and error monitoring, Autumn and Stripe for hosted billing and payment processing, Cloudflare for hosting, storage, and access controls, and DataForSEO for data requested through the Service. Those vendors use your Personal Information only at our direction and in accordance with our Privacy Policy. +Except as otherwise stated in this Privacy Policy, we do not sell, trade, rent or otherwise share for marketing purposes your Personal Information with third parties without your consent. We do share Personal Information with vendors who are performing services for the Company, such as providers that help us send email communications, operate analytics, measure advertising conversions, process billing, provide data requested through the Service, and host or support the infrastructure behind the Site and Service. These vendors may include Loops for email communications, Plausible for public-site analytics, PostHog for hosted analytics, session replay, and error monitoring, Reddit Ads for advertising conversion measurement, Autumn and Stripe for hosted billing and payment processing, Cloudflare for hosting, storage, and access controls, and DataForSEO for data requested through the Service. Those vendors use your Personal Information only at our direction and in accordance with our Privacy Policy. In general, the Personal Information you provide to us is used to help us communicate with you and operate the Service. For example, we use Personal Information to create and secure accounts, authenticate users, provide technical support, process billing, send administrative or transactional emails, contact users in response to questions, solicit feedback from users, and inform users about product updates and promotional offers. We may share Personal Information with outside parties if we have a good-faith belief that access, use, preservation or disclosure of the information is reasonably necessary to meet any applicable legal process or enforceable governmental request; to enforce applicable Terms of Service, including investigation of potential violations; address fraud, security or technical concerns; or to protect against harm to the rights, property, or safety of our users or the public as required or permitted by law.