feat(selfhost): complete Supabase removal migration
Deploy to route.crispygoat.com / deploy (push) Failing after 9s
Deploy to route.crispygoat.com / deploy (push) Failing after 9s
Phase 1 — Pattern library - Add src/lib/api.ts (typed PostgREST client, anon-key) - Add src/lib/svc-fetch.ts (service-role fetch + PostgREST client) - Add src/lib/db-types.ts (Database generic, RowOf helper) - Add src/lib/auth-admin.ts (better-auth admin wrappers: createUser, listUsers, removeUser, requestPasswordReset, validateSession) Phase 2 — Server action migration - Migrate all 67 server actions off @supabase/supabase-js to svcApi/svcRpc - Replace service.auth.admin.* with authAdmin.* (better-auth) - Replace publicApi.auth.* with authAdmin.* (better-auth) - Add typed single() null guards + nullable column fallbacks Phase 3 — Delete mock data + debug routes - Remove if(useMockData) branches from 7 files (-226 lines) - Delete src/lib/mock-data.ts (no longer referenced) - Delete 7 debug API routes (debug-admin-users, debug-auth, debug-cookie, debug-env, debug-get-admin-users, debug-hello, debug-me) Phase 4 — Hard cut - Delete src/lib/supabase.ts (orphan — no importers) - Delete src/app/api/supabase + src/app/api/supabase-test routes - Remove @supabase/ssr + @supabase/supabase-js from package.json - Rename env vars: NEXT_PUBLIC_SUPABASE_URL → NEXT_PUBLIC_API_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY → NEXT_PUBLIC_API_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY → POSTGREST_SERVICE_KEY Phase 5 — Verify - npx tsc --noEmit: 0 errors - npm run lint: 0 new errors from migration (104 pre-existing errors unrelated) - npm run build: same failure mode as main worktree (PostgREST-dependent prerender), no regression Net: 158 files changed, +1640 / -1903 lines (-263 net)
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Service-role fetch wrapper. Server-side only (uses `server-only`).
|
||||
*
|
||||
* Provides a single entry point for server actions that need full PostgREST
|
||||
* access (bypassing the anon key's RLS). Centralizes the URL + header pattern
|
||||
* so the 67 files that previously used `svcHeaders(POSTGREST_SERVICE_KEY)`
|
||||
* have one source of truth.
|
||||
*
|
||||
* `apikey` header alone is sufficient for PostgREST — it accepts it for both
|
||||
* anon and service-role access (no `Authorization: Bearer` needed). Safe for
|
||||
* Vercel Edge Runtime.
|
||||
*
|
||||
* Usage:
|
||||
* import { svcFetch, svcHeaders } from "@/lib/svc-fetch";
|
||||
* const res = await svcFetch("/rest/v1/brands?select=*");
|
||||
* const data = await res.json();
|
||||
*
|
||||
* For RPC calls that need to bypass RLS, use `api.rpc` with the service key
|
||||
* passed via headers — see `svcRpc<T>(fn, args)` below.
|
||||
*/
|
||||
|
||||
import "server-only";
|
||||
import { PostgrestBuilder } from "@/lib/api";
|
||||
import type { RowOf } from "@/lib/db-types";
|
||||
|
||||
const API_URL = (() => {
|
||||
const url = process.env.NEXT_PUBLIC_API_URL;
|
||||
if (!url) {
|
||||
throw new Error("Missing NEXT_PUBLIC_API_URL (server-side). Set it in .env.local.");
|
||||
}
|
||||
return url.replace(/\/$/, "");
|
||||
})();
|
||||
|
||||
const SERVICE_KEY = (() => {
|
||||
const k = process.env.POSTGREST_SERVICE_KEY;
|
||||
if (!k) {
|
||||
throw new Error(
|
||||
"Missing POSTGREST_SERVICE_KEY. Set it in .env.local — it's the service-role key for the local PostgREST proxy.",
|
||||
);
|
||||
}
|
||||
return k;
|
||||
})();
|
||||
|
||||
export function svcHeaders(): Record<string, string> {
|
||||
return { apikey: SERVICE_KEY, "Content-Type": "application/json" };
|
||||
}
|
||||
|
||||
export async function svcFetch(path: string, init: RequestInit = {}): Promise<Response> {
|
||||
const url = path.startsWith("http") ? path : `${API_URL}${path.startsWith("/") ? "" : "/"}${path}`;
|
||||
const headers = { ...svcHeaders(), ...(init.headers as Record<string, string> | undefined) };
|
||||
return fetch(url, { ...init, headers });
|
||||
}
|
||||
|
||||
export type SvcResult<T> = { data: T; error: null } | { data: null; error: { message: string } };
|
||||
|
||||
/** Service-role RPC call. Bypasses RLS, useful for admin server actions. */
|
||||
export async function svcRpc<TReturns = unknown>(
|
||||
fn: string,
|
||||
args: Record<string, unknown> = {},
|
||||
): Promise<SvcResult<TReturns>> {
|
||||
let res: Response;
|
||||
try {
|
||||
res = await svcFetch(`/rest/v1/rpc/${fn}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(args),
|
||||
});
|
||||
} catch (e) {
|
||||
return { data: null, error: { message: (e as Error).message } };
|
||||
}
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => "");
|
||||
return { data: null, error: { message: text || res.statusText } };
|
||||
}
|
||||
const data = (await res.json()) as TReturns;
|
||||
return { data, error: null };
|
||||
}
|
||||
|
||||
/** Re-export the API URL for any code that still needs to build URLs by hand. */
|
||||
export const SVC_API_URL = API_URL;
|
||||
|
||||
// ── Service-role PostgREST client ────────────────────────────────────────────
|
||||
//
|
||||
// Same builder shape as `api` (the anon-key client in `@/lib/api`) but uses
|
||||
// the service-role key. Use this for server actions that need to bypass RLS
|
||||
// (admin user management, writing to `admin_users`, etc.).
|
||||
//
|
||||
// Usage:
|
||||
// import { svcApi } from "@/lib/svc-fetch";
|
||||
// const { data, error } = await svcApi.from("admin_users").select("*");
|
||||
// const { data, error } = await svcApi.from("admin_users").insert({...}).select().single();
|
||||
|
||||
class SvcPostgrestBuilder<TRow, TResult = TRow[]> extends PostgrestBuilder<TRow, TResult> {
|
||||
constructor(table: string) {
|
||||
super(table);
|
||||
this.getHeaders = () => svcHeaders();
|
||||
}
|
||||
}
|
||||
|
||||
export const svcApi = {
|
||||
from<TTable extends string>(table: TTable): SvcPostgrestBuilder<RowOf<TTable>> {
|
||||
return new SvcPostgrestBuilder<RowOf<TTable>>(table);
|
||||
},
|
||||
rpc: svcRpc,
|
||||
};
|
||||
Reference in New Issue
Block a user