From 18904af98f8f61fdacc546637619c58c940697dd Mon Sep 17 00:00:00 2001 From: rebeccafitzpatr Date: Sun, 24 May 2026 16:08:02 +1200 Subject: [PATCH 1/7] only access supabase and prisma within methods --- src/app/admin/page.tsx | 1 + src/app/api/admin/approve/route.ts | 4 +-- src/app/api/admin/claims/route.ts | 4 ++- src/app/api/admin/revoke/route.ts | 4 ++- src/app/api/claims/route.ts | 7 +++- src/lib/prisma.ts | 33 +++++++++++-------- .../registrations/registrations-service.tsx | 24 ++++++++------ src/services/supabase.tsx | 14 +++++--- 8 files changed, 58 insertions(+), 33 deletions(-) diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 7d7b3e1..074c7ea 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -6,6 +6,7 @@ import { useSignMessage } from "wagmi"; import { Button } from "@/components/ui/button"; import { WalletButton } from "@/components/wallet-button"; +export const dynamic = "force-dynamic"; export default function AdminPage() { const { address, isConnected, mounted } = useWallet(); const { signMessageAsync } = useSignMessage(); diff --git a/src/app/api/admin/approve/route.ts b/src/app/api/admin/approve/route.ts index afb0f13..342be81 100644 --- a/src/app/api/admin/approve/route.ts +++ b/src/app/api/admin/approve/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { prisma } from "@/lib/prisma"; +import { getPrisma } from "@/lib/prisma"; import { setName } from "@/lib/namestone"; import { verifyMessage } from "viem"; import { isAllowedAdminAddress } from "@/lib/admin-auth"; @@ -28,7 +28,7 @@ export async function POST(req: NextRequest) { const isAuth = await verifyAdminAuth(req); if (!isAuth) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - + const prisma = getPrisma(); try { const { claimId } = await req.json(); diff --git a/src/app/api/admin/claims/route.ts b/src/app/api/admin/claims/route.ts index dd47c62..b10cb80 100644 --- a/src/app/api/admin/claims/route.ts +++ b/src/app/api/admin/claims/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { prisma } from "@/lib/prisma"; +import { getPrisma } from "@/lib/prisma"; import { verifyMessage } from "viem"; import { isAllowedAdminAddress } from "@/lib/admin-auth"; @@ -36,6 +36,7 @@ export async function GET(req: NextRequest) { const { searchParams } = new URL(req.url); const status = searchParams.get("status"); + const prisma = getPrisma(); try { const claims = await prisma.claimRequest.findMany({ where: status ? { status } : undefined, @@ -56,6 +57,7 @@ export async function PUT(req: NextRequest) { if (!isAuth) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + const prisma = getPrisma(); try { const body = await req.json(); const { id, status } = body; // status can be "APPROVED", "REJECTED" diff --git a/src/app/api/admin/revoke/route.ts b/src/app/api/admin/revoke/route.ts index ad2ffff..22fd635 100644 --- a/src/app/api/admin/revoke/route.ts +++ b/src/app/api/admin/revoke/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { prisma } from "@/lib/prisma"; +import { getPrisma } from "@/lib/prisma"; import { deleteName } from "@/lib/namestone"; import { verifyMessage } from "viem"; import { isAllowedAdminAddress } from "@/lib/admin-auth"; @@ -29,6 +29,8 @@ export async function POST(req: NextRequest) { if (!isAuth) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + const prisma = getPrisma(); + try { const { name, claimId } = await req.json(); diff --git a/src/app/api/claims/route.ts b/src/app/api/claims/route.ts index 0b11a3b..d9d9d5d 100644 --- a/src/app/api/claims/route.ts +++ b/src/app/api/claims/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { prisma } from "@/lib/prisma"; +import { getPrisma } from "@/lib/prisma"; export async function GET(req: NextRequest) { const { searchParams } = new URL(req.url); @@ -9,6 +9,8 @@ export async function GET(req: NextRequest) { return NextResponse.json({ error: "Address is required" }, { status: 400 }); } + const prisma = getPrisma(); + try { const claims = await prisma.claimRequest.findMany({ where: { @@ -29,6 +31,9 @@ export async function GET(req: NextRequest) { } export async function POST(req: NextRequest) { + + const prisma = getPrisma(); + try { const body = await req.json(); const { walletAddress, requestedName } = body; diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index f81e1cc..0416cd9 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -7,7 +7,7 @@ const normalizeDatabaseUrl = (value?: string) => { return value.trim().replace(/^['\"]+|['\"]+$/g, ""); }; -const prismaClientSingleton = () => { +const createPrismaClient = () => { const databaseUrl = normalizeDatabaseUrl(process.env.DATABASE_URL); if (!databaseUrl) { @@ -24,21 +24,28 @@ const prismaClientSingleton = () => { } if (protocol !== "postgresql:" && protocol !== "postgres:") { - throw new Error( - "DATABASE_URL must start with postgresql:// or postgres://", - ); + throw new Error("DATABASE_URL must start with postgresql:// or postgres://"); } const pool = new Pool({ connectionString: databaseUrl }); const adapter = new PrismaPg(pool); - const client = new PrismaClient({ adapter }); - return client; + return new PrismaClient({ adapter }); }; -declare const globalThis: { - prismaGlobal: ReturnType; -} & typeof global; - -export const prisma = globalThis.prismaGlobal ?? prismaClientSingleton(); - -if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = prisma; +declare global { + // eslint-disable-next-line no-var + var prismaGlobal: PrismaClient | undefined; +} + +/** + * Lazy Prisma accessor. + * Safe to import during `next build` because it does not read env / connect + * until you call it inside a request handler. + */ +export const getPrisma = (): PrismaClient => { + if (process.env.NODE_ENV !== "production") { + globalThis.prismaGlobal ??= createPrismaClient(); + return globalThis.prismaGlobal; + } + return createPrismaClient(); +}; diff --git a/src/services/registrations/registrations-service.tsx b/src/services/registrations/registrations-service.tsx index 573e965..9c901b9 100644 --- a/src/services/registrations/registrations-service.tsx +++ b/src/services/registrations/registrations-service.tsx @@ -1,10 +1,11 @@ import { RegistrationData } from "../../lib/schemas/registration"; -import { supabase } from "../supabase"; +import { getSupabase } from "../supabase"; const PROFILE_PICTURE_BUCKET = "profile_pictures"; - +export const dynamic = "force-dynamic"; export const RegistrationService = { submitRegistration: async (registrationData: RegistrationData) => { + const supabase = getSupabase(); const { data, error } = await supabase .from("registrations") .insert([registrationData]) @@ -19,7 +20,7 @@ export const RegistrationService = { isEmailTaken: async (email: string) => { const cleanEmail = email.trim().toLowerCase(); - + const supabase = getSupabase(); const { data, error } = await supabase.rpc("is_email_registered", { search_email: cleanEmail, }); @@ -33,7 +34,7 @@ export const RegistrationService = { isWalletEmpty: async (email: string) => { const cleanEmail = email.trim().toLowerCase(); - + const supabase = getSupabase(); const { data, error } = await supabase.rpc("is_wallet_id_empty", { input_email: cleanEmail, }); @@ -47,7 +48,7 @@ export const RegistrationService = { isWalletRegistered: async (walletAddress: string) => { const cleanWalletAddress = walletAddress.trim().toLowerCase(); - + const supabase = getSupabase(); const { data, error } = await supabase.rpc("is_wallet_registered", { search_wallet: cleanWalletAddress, }); @@ -62,7 +63,7 @@ export const RegistrationService = { linkWalletToEmail: async (email: string, walletAddress: string) => { const cleanEmail = email.trim().toLowerCase(); const cleanWalletAddress = walletAddress.trim().toLowerCase(); - + const supabase = getSupabase(); const { data, error } = await supabase.rpc("link_wallet_to_email", { input_email: cleanEmail, input_wallet: cleanWalletAddress, @@ -90,11 +91,12 @@ export const RegistrationService = { }, uploadProfilePicture: async (email: string, file: File) => { + const supabase = getSupabase(); const profilePicturePath = RegistrationService.getProfilePicturePath( email, file, ); - + const { error } = await supabase.storage .from(PROFILE_PICTURE_BUCKET) .upload(profilePicturePath, file, { @@ -128,6 +130,7 @@ export const RegistrationService = { profilePicturePath?: string; profilePictureUrl?: string; }) => { + const supabase = getSupabase(); const cleanEmail = email.trim().toLowerCase(); const updateData: { @@ -163,6 +166,7 @@ export const RegistrationService = { }, getRegistrationByWallet: async (walletAddress: string) => { + const supabase = getSupabase(); const cleanWalletAddress = RegistrationService.normalizeWalletAddress(walletAddress); @@ -251,7 +255,7 @@ export const RegistrationService = { getRegistrationByEmail: async (email: string) => { const cleanEmail = email.trim().toLowerCase(); - + const supabase = getSupabase(); const { data, error } = await supabase .from("registrations") .select("*") @@ -267,7 +271,7 @@ export const RegistrationService = { updateBadges: async (email: string, badges: string[]) => { const cleanEmail = email.trim().toLowerCase(); - + const supabase = getSupabase(); const { data, error } = await supabase .from("registrations") .update({ @@ -332,7 +336,7 @@ export const RegistrationService = { updateEventsAttended: async (email: string, eventsAttended: string[]) => { const cleanEmail = email.trim().toLowerCase(); - + const supabase = getSupabase(); const { data, error } = await supabase .from("registrations") .update({ diff --git a/src/services/supabase.tsx b/src/services/supabase.tsx index 0edc998..29e9b17 100644 --- a/src/services/supabase.tsx +++ b/src/services/supabase.tsx @@ -1,4 +1,5 @@ -import { createClient } from "@supabase/supabase-js"; +import { createClient, SupabaseClient } from "@supabase/supabase-js"; +let _supabase: SupabaseClient | null = null; /** * Creates and exports a Supabase client instance. @@ -7,17 +8,20 @@ import { createClient } from "@supabase/supabase-js"; * * @see https://supabase.com/docs/client/imports */ -export const supabase = (() => { +export function getSupabase(): SupabaseClient { + if (_supabase) return _supabase; + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL?.trim(); const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY?.trim(); if (!supabaseUrl || !supabaseAnonKey) { throw new Error( "Missing Supabase environment variables " + - (supabaseUrl ? "" : "NEXT_PUBLIC_SUPABASE_URL") + + (supabaseUrl ? "" : "NEXT_PUBLIC_SUPABASE_URL ") + (supabaseAnonKey ? "" : "NEXT_PUBLIC_SUPABASE_ANON_KEY"), ); } - return createClient(supabaseUrl, supabaseAnonKey); -})(); + _supabase = createClient(supabaseUrl, supabaseAnonKey); + return _supabase; +} From 10d77e55002bfa167c6664279be54126bea7e118 Mon Sep 17 00:00:00 2001 From: rebeccafitzpatr Date: Sun, 24 May 2026 21:32:28 +1200 Subject: [PATCH 2/7] add vars to remote buid --- .github/workflows/fly-deploy.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/fly-deploy.yml b/.github/workflows/fly-deploy.yml index c5c390b..acbd38b 100644 --- a/.github/workflows/fly-deploy.yml +++ b/.github/workflows/fly-deploy.yml @@ -14,6 +14,10 @@ jobs: steps: - uses: actions/checkout@v4 - uses: superfly/flyctl-actions/setup-flyctl@master - - run: flyctl deploy --remote-only + - run: > + flyctl deploy --remote-only + --build-arg NEXT_PUBLIC_SUPABASE_URL=${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} + --build-arg NEXT_PUBLIC_SUPABASE_ANON_KEY=${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }} + --build-arg NEXT_PUBLIC_REOWN_PROJECT_ID=${{ secrets.NEXT_PUBLIC_REOWN_PROJECT_ID }} env: FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} From 021c9717d6eed100c36e2b0b549d72fbd8006b33 Mon Sep 17 00:00:00 2001 From: rebeccafitzpatr Date: Sun, 24 May 2026 21:42:45 +1200 Subject: [PATCH 3/7] add vars to dockerfile --- Dockerfile | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Dockerfile b/Dockerfile index ba80422..2778474 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,8 +32,15 @@ COPY . . # inlines NEXT_PUBLIC_* values into the client bundle during next build. ARG NEXT_PUBLIC_ADMIN_ADDRESS ARG NEXT_PUBLIC_ADMIN_ADDRESSES +ARG NEXT_PUBLIC_SUPABASE_URL +ARG NEXT_PUBLIC_SUPABASE_ANON_KEY +ARG NEXT_PUBLIC_REOWN_PROJECT_ID + ENV NEXT_PUBLIC_ADMIN_ADDRESS="${NEXT_PUBLIC_ADMIN_ADDRESS}" ENV NEXT_PUBLIC_ADMIN_ADDRESSES="${NEXT_PUBLIC_ADMIN_ADDRESSES}" +ENV NEXT_PUBLIC_SUPABASE_URL="${NEXT_PUBLIC_SUPABASE_URL}" +ENV NEXT_PUBLIC_SUPABASE_ANON_KEY="${NEXT_PUBLIC_SUPABASE_ANON_KEY}" +ENV NEXT_PUBLIC_REOWN_PROJECT_ID="${NEXT_PUBLIC_REOWN_PROJECT_ID}" # Build application (runs prisma generate + prisma:sync + next build via package.json) RUN bun run build From 68bbbf1562b14a3bd526adbbb457d52bfaaf65a0 Mon Sep 17 00:00:00 2001 From: rebeccafitzpatr Date: Wed, 27 May 2026 19:25:54 +1200 Subject: [PATCH 4/7] add to docker ignore --- .dockerignore | 25 +++++++++++++++++++++++-- .github/workflows/fly-deploy.yml | 12 +++++++----- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/.dockerignore b/.dockerignore index 883b43c..4d95e37 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,12 +1,33 @@ +# VCS / CI .git .github + +# Build output .next +dist +out +coverage + +# Dependencies node_modules + +# Logs npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* bun-debug.log* + +# Local env (keep .env.example) .env .env.* !.env.example -Dockerfile -README.md + +# Tooling caches tsconfig.tsbuildinfo +.turbo +.cache + +# OS/editor junk +.DS_Store +.vscode \ No newline at end of file diff --git a/.github/workflows/fly-deploy.yml b/.github/workflows/fly-deploy.yml index acbd38b..89a7c7b 100644 --- a/.github/workflows/fly-deploy.yml +++ b/.github/workflows/fly-deploy.yml @@ -14,10 +14,12 @@ jobs: steps: - uses: actions/checkout@v4 - uses: superfly/flyctl-actions/setup-flyctl@master - - run: > - flyctl deploy --remote-only - --build-arg NEXT_PUBLIC_SUPABASE_URL=${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} - --build-arg NEXT_PUBLIC_SUPABASE_ANON_KEY=${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }} - --build-arg NEXT_PUBLIC_REOWN_PROJECT_ID=${{ secrets.NEXT_PUBLIC_REOWN_PROJECT_ID }} + - run: | + flyctl deploy --remote-only --verbose \ + --build-arg NEXT_PUBLIC_SUPABASE_URL="${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}" \ + --build-arg NEXT_PUBLIC_SUPABASE_ANON_KEY="${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}" \ + --build-arg NEXT_PUBLIC_REOWN_PROJECT_ID="${{ secrets.NEXT_PUBLIC_REOWN_PROJECT_ID }}" \ + --build-arg NEXT_PUBLIC_ADMIN_ADDRESS="${{ secrets.NEXT_PUBLIC_ADMIN_ADDRESS }}" \ + --build-arg NEXT_PUBLIC_ADMIN_ADDRESSES="${{ secrets.NEXT_PUBLIC_ADMIN_ADDRESSES }}" env: FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} From a6dde039750ab6d2c787d234ac14e0496d4b46c2 Mon Sep 17 00:00:00 2001 From: rebeccafitzpatr Date: Wed, 27 May 2026 23:19:56 +1200 Subject: [PATCH 5/7] call only one prisma client --- src/lib/prisma.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 0416cd9..7f17a0a 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -43,9 +43,6 @@ declare global { * until you call it inside a request handler. */ export const getPrisma = (): PrismaClient => { - if (process.env.NODE_ENV !== "production") { - globalThis.prismaGlobal ??= createPrismaClient(); + globalThis.prismaGlobal ??= createPrismaClient(); return globalThis.prismaGlobal; - } - return createPrismaClient(); }; From 28379abd4fc0ad84c0aa53ed87449fe0d0437a5d Mon Sep 17 00:00:00 2001 From: rebeccafitzpatr Date: Thu, 28 May 2026 09:32:02 +1200 Subject: [PATCH 6/7] add pr-check --- .github/workflows/pr-check.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/pr-check.yml diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml new file mode 100644 index 0000000..996f89e --- /dev/null +++ b/.github/workflows/pr-check.yml @@ -0,0 +1,23 @@ +name: PR Build + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +jobs: + build: + name: Build Docker image (PR) + runs-on: ubuntu-latest + if: github.event.pull_request.draft == false + steps: + - uses: actions/checkout@v4 + + - name: Docker build (validates bun run build in Dockerfile) + run: | + docker build \ + --build-arg NEXT_PUBLIC_SUPABASE_URL="${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}" \ + --build-arg NEXT_PUBLIC_SUPABASE_ANON_KEY="${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}" \ + --build-arg NEXT_PUBLIC_REOWN_PROJECT_ID="${{ secrets.NEXT_PUBLIC_REOWN_PROJECT_ID }}" \ + --build-arg NEXT_PUBLIC_ADMIN_ADDRESS="${{ secrets.NEXT_PUBLIC_ADMIN_ADDRESS }}" \ + --build-arg NEXT_PUBLIC_ADMIN_ADDRESSES="${{ secrets.NEXT_PUBLIC_ADMIN_ADDRESSES }}" \ + -t web3copy:pr-${{ github.event.pull_request.number }} . \ No newline at end of file From ae5b19dff4907c4dd5e8081deddda3e4c51b0538 Mon Sep 17 00:00:00 2001 From: rebeccafitzpatr Date: Thu, 28 May 2026 09:40:57 +1200 Subject: [PATCH 7/7] move prisma inside try block --- src/app/api/admin/approve/route.ts | 3 ++- src/app/api/admin/claims/route.ts | 6 ++++-- src/app/api/admin/revoke/route.ts | 3 ++- src/app/api/claims/route.ts | 5 ++--- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/app/api/admin/approve/route.ts b/src/app/api/admin/approve/route.ts index 342be81..bdaefc3 100644 --- a/src/app/api/admin/approve/route.ts +++ b/src/app/api/admin/approve/route.ts @@ -28,8 +28,9 @@ export async function POST(req: NextRequest) { const isAuth = await verifyAdminAuth(req); if (!isAuth) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - const prisma = getPrisma(); + try { + const prisma = getPrisma(); const { claimId } = await req.json(); const claim = await prisma.claimRequest.findUnique({ diff --git a/src/app/api/admin/claims/route.ts b/src/app/api/admin/claims/route.ts index b10cb80..bc9ff93 100644 --- a/src/app/api/admin/claims/route.ts +++ b/src/app/api/admin/claims/route.ts @@ -36,8 +36,9 @@ export async function GET(req: NextRequest) { const { searchParams } = new URL(req.url); const status = searchParams.get("status"); - const prisma = getPrisma(); + try { + const prisma = getPrisma(); const claims = await prisma.claimRequest.findMany({ where: status ? { status } : undefined, orderBy: { createdAt: "desc" }, @@ -57,8 +58,9 @@ export async function PUT(req: NextRequest) { if (!isAuth) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - const prisma = getPrisma(); + try { + const prisma = getPrisma(); const body = await req.json(); const { id, status } = body; // status can be "APPROVED", "REJECTED" diff --git a/src/app/api/admin/revoke/route.ts b/src/app/api/admin/revoke/route.ts index 22fd635..ab0a902 100644 --- a/src/app/api/admin/revoke/route.ts +++ b/src/app/api/admin/revoke/route.ts @@ -29,9 +29,10 @@ export async function POST(req: NextRequest) { if (!isAuth) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - const prisma = getPrisma(); + try { + const prisma = getPrisma(); const { name, claimId } = await req.json(); // Delete from NameStone diff --git a/src/app/api/claims/route.ts b/src/app/api/claims/route.ts index d9d9d5d..05e7e31 100644 --- a/src/app/api/claims/route.ts +++ b/src/app/api/claims/route.ts @@ -9,9 +9,9 @@ export async function GET(req: NextRequest) { return NextResponse.json({ error: "Address is required" }, { status: 400 }); } - const prisma = getPrisma(); try { + const prisma = getPrisma(); const claims = await prisma.claimRequest.findMany({ where: { walletAddress: address.toLowerCase(), @@ -32,9 +32,8 @@ export async function GET(req: NextRequest) { export async function POST(req: NextRequest) { - const prisma = getPrisma(); - try { + const prisma = getPrisma(); const body = await req.json(); const { walletAddress, requestedName } = body; const normalizedWalletAddress = walletAddress?.toLowerCase();