diff --git a/lib/supabase.ts b/lib/supabase.ts new file mode 100644 index 00000000..65d9b777 --- /dev/null +++ b/lib/supabase.ts @@ -0,0 +1,173 @@ +import { createClient, SupabaseClient } from "@supabase/supabase-js"; + +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || ""; +const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || ""; + +// Browser-side client (anon key, respects RLS) +export const supabase: SupabaseClient | null = + supabaseUrl && supabaseAnonKey + ? createClient(supabaseUrl, supabaseAnonKey) + : null; + +// Server-side client (service role, bypasses RLS) +export function createServerClient(): SupabaseClient | null { + const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY || ""; + if (!supabaseUrl || !serviceRoleKey) return null; + + return createClient(supabaseUrl, serviceRoleKey, { + auth: { + autoRefreshToken: false, + persistSession: false, + }, + }); +} + +// --------------------------------------------------------------------------- +// Database types +// --------------------------------------------------------------------------- + +export type Json = + | string + | number + | boolean + | null + | { [key: string]: Json | undefined } + | Json[]; + +export interface Database { + public: { + Tables: { + storylines: { + Row: { + id: number; + storyline_id: number; + writer_address: string; + token_address: string; + title: string; + plot_count: number; + last_plot_time: string | null; + has_deadline: boolean; + sunset: boolean; + writer_type: number | null; + hidden: boolean; + tx_hash: string; + log_index: number; + block_timestamp: string | null; + indexed_at: string; + }; + Insert: { + id?: never; + storyline_id: number; + writer_address: string; + token_address: string; + title: string; + plot_count?: number; + last_plot_time?: string | null; + has_deadline?: boolean; + sunset?: boolean; + writer_type?: number | null; + hidden?: boolean; + tx_hash: string; + log_index: number; + block_timestamp?: string | null; + indexed_at?: string; + }; + Update: { + id?: never; + storyline_id?: number; + writer_address?: string; + token_address?: string; + title?: string; + plot_count?: number; + last_plot_time?: string | null; + has_deadline?: boolean; + sunset?: boolean; + writer_type?: number | null; + hidden?: boolean; + tx_hash?: string; + log_index?: number; + block_timestamp?: string | null; + indexed_at?: string; + }; + }; + plots: { + Row: { + id: number; + storyline_id: number; + plot_index: number; + writer_address: string; + content_cid: string; + content_hash: string; + hidden: boolean; + tx_hash: string; + log_index: number; + block_timestamp: string | null; + indexed_at: string; + }; + Insert: { + id?: never; + storyline_id: number; + plot_index: number; + writer_address: string; + content_cid: string; + content_hash: string; + hidden?: boolean; + tx_hash: string; + log_index: number; + block_timestamp?: string | null; + indexed_at?: string; + }; + Update: { + id?: never; + storyline_id?: number; + plot_index?: number; + writer_address?: string; + content_cid?: string; + content_hash?: string; + hidden?: boolean; + tx_hash?: string; + log_index?: number; + block_timestamp?: string | null; + indexed_at?: string; + }; + }; + donations: { + Row: { + id: number; + storyline_id: number; + donor_address: string; + amount: string; + tx_hash: string; + log_index: number; + block_timestamp: string | null; + indexed_at: string; + }; + Insert: { + id?: never; + storyline_id: number; + donor_address: string; + amount: string; + tx_hash: string; + log_index: number; + block_timestamp?: string | null; + indexed_at?: string; + }; + Update: { + id?: never; + storyline_id?: number; + donor_address?: string; + amount?: string; + tx_hash?: string; + log_index?: number; + block_timestamp?: string | null; + indexed_at?: string; + }; + }; + }; + }; +} + +// Convenience type aliases +export type Storyline = Database["public"]["Tables"]["storylines"]["Row"]; +export type Plot = Database["public"]["Tables"]["plots"]["Row"]; +export type Donation = Database["public"]["Tables"]["donations"]["Row"]; diff --git a/package-lock.json b/package-lock.json index 0c6a24f9..93d55a13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "plotlink", "version": "0.1.0", "dependencies": { + "@supabase/supabase-js": "^2.99.1", "next": "16.1.6", "react": "19.2.3", "react-dom": "19.2.3" @@ -1233,6 +1234,86 @@ "dev": true, "license": "MIT" }, + "node_modules/@supabase/auth-js": { + "version": "2.99.1", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.99.1.tgz", + "integrity": "sha512-x7lKKTvKjABJt/FYcRSPiTT01Xhm2FF8RhfL8+RHMkmlwmRQ88/lREupIHKwFPW0W6pTCJqkZb7Yhpw/EZ+fNw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.99.1", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.99.1.tgz", + "integrity": "sha512-WQE62W5geYImCO4jzFxCk/avnK7JmOdtqu2eiPz3zOaNiIJajNRSAwMMDgEGd2EMs+sUVYj1LfBjfmW3EzHgIA==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.99.1", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.99.1.tgz", + "integrity": "sha512-gtw2ibJrADvfqrpUWXGNlrYUvxttF4WVWfPpTFKOb2IRj7B6YRWMDgcrYqIuD4ZEabK4m6YKQCCGy6clgf1lPA==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.99.1", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.99.1.tgz", + "integrity": "sha512-9EDdy/5wOseGFqxW88ShV9JMRhm7f+9JGY5x+LqT8c7R0X1CTLwg5qie8FiBWcXTZ+68yYxVWunI+7W4FhkWOg==", + "license": "MIT", + "dependencies": { + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.99.1", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.99.1.tgz", + "integrity": "sha512-mf7zPfqofI62SOoyQJeNUVxe72E4rQsbWim6lTDPeLu3lHija/cP5utlQADGrjeTgOUN6znx/rWn7SjrETP1dw==", + "license": "MIT", + "dependencies": { + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.99.1", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.99.1.tgz", + "integrity": "sha512-5MRoYD9ffXq8F6a036dm65YoSHisC3by/d22mauKE99Vrwf792KxYIIr/iqCX7E4hkuugbPZ5EGYHTB7MKy6Vg==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.99.1", + "@supabase/functions-js": "2.99.1", + "@supabase/postgrest-js": "2.99.1", + "@supabase/realtime-js": "2.99.1", + "@supabase/storage-js": "2.99.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1549,12 +1630,17 @@ "version": "20.19.37", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/phoenix": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz", + "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", @@ -1575,6 +1661,15 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.57.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", @@ -3926,6 +4021,15 @@ "hermes-estree": "0.25.1" } }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -6352,7 +6456,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unrs-resolver": { @@ -6546,6 +6649,27 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 8f0ee326..3baff89e 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@supabase/supabase-js": "^2.99.1", "next": "16.1.6", "react": "19.2.3", "react-dom": "19.2.3" diff --git a/supabase/migrations/00001_schema.sql b/supabase/migrations/00001_schema.sql new file mode 100644 index 00000000..eecb10e7 --- /dev/null +++ b/supabase/migrations/00001_schema.sql @@ -0,0 +1,67 @@ +-- PlotLink schema (§4.1) +-- Tables: storylines, plots, donations + +-- --------------------------------------------------------------------------- +-- storylines +-- --------------------------------------------------------------------------- +create table storylines ( + id bigint generated always as identity primary key, + storyline_id bigint not null, + writer_address text not null, + token_address text not null, + title text not null, + plot_count integer not null default 0, + last_plot_time timestamptz, + has_deadline boolean not null default false, + sunset boolean not null default false, + writer_type smallint, -- NULL = unclassified, 0 = human, 1 = agent (set by indexer) + hidden boolean not null default false, -- MVP content moderation (§8) + tx_hash text not null, + log_index integer not null, + block_timestamp timestamptz, + indexed_at timestamptz not null default now(), + + constraint storylines_tx_unique unique (tx_hash, log_index), + constraint storylines_onchain_unique unique (storyline_id) +); + +create index idx_storylines_writer on storylines (writer_address); + +-- --------------------------------------------------------------------------- +-- plots +-- --------------------------------------------------------------------------- +create table plots ( + id bigint generated always as identity primary key, + storyline_id bigint not null references storylines (storyline_id), + plot_index integer not null, + writer_address text not null, + content_cid text not null, + content_hash text not null, + hidden boolean not null default false, -- MVP content moderation (§8) + tx_hash text not null, + log_index integer not null, + block_timestamp timestamptz, + indexed_at timestamptz not null default now(), + + constraint plots_tx_unique unique (tx_hash, log_index) +); + +create index idx_plots_storyline on plots (storyline_id); + +-- --------------------------------------------------------------------------- +-- donations +-- --------------------------------------------------------------------------- +create table donations ( + id bigint generated always as identity primary key, + storyline_id bigint not null references storylines (storyline_id), + donor_address text not null, + amount text not null, -- wei string to avoid precision loss + tx_hash text not null, + log_index integer not null, + block_timestamp timestamptz, + indexed_at timestamptz not null default now(), + + constraint donations_tx_unique unique (tx_hash, log_index) +); + +create index idx_donations_storyline on donations (storyline_id);