feat: Add D1 database integration with proper Cloudflare Workers support

- Update prisma.ts to use getPrismaWithD1() with getCloudflareContext()
- Update all API routes to use async D1 connection
- Add open-next.config.ts for Cloudflare Workers deployment
- Add wrangler.jsonc with D1 binding and custom domain routes
- Fix TypeScript type errors in API routes and storage
- Add @workos-inc/authkit-nextjs dependency
- Remove incompatible prisma.config.ts
This commit is contained in:
Avery Felts 2026-01-25 17:21:36 -07:00
parent 92c710287b
commit 14c45eeb24
18 changed files with 2518 additions and 88 deletions

4
.gitignore vendored
View File

@ -44,3 +44,7 @@ next-env.d.ts
# database
*.db
*.db-journal
# cloudflare
.open-next/
.wrangler/

2139
bun.lock

File diff suppressed because it is too large Load Diff

10
env.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
/// <reference types="@cloudflare/workers-types" />
declare global {
interface CloudflareEnv {
DB: D1Database;
ASSETS: Fetcher;
}
}
export { };

View File

@ -1,7 +1,8 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
// Required for OpenNext/Cloudflare Workers
serverExternalPackages: ["@prisma/client"],
};
export default nextConfig;

28
open-next.config.ts Normal file
View File

@ -0,0 +1,28 @@
import type { OpenNextConfig } from "@opennextjs/cloudflare";
const config: OpenNextConfig = {
default: {
override: {
wrapper: "cloudflare-node",
converter: "edge",
proxyExternalRequest: "fetch",
incrementalCache: "dummy",
tagCache: "dummy",
queue: "dummy",
},
},
edgeExternals: ["node:crypto"],
middleware: {
external: true,
override: {
wrapper: "cloudflare-edge",
converter: "edge",
proxyExternalRequest: "fetch",
incrementalCache: "dummy",
tagCache: "dummy",
queue: "dummy",
},
},
};
export default config;

View File

@ -7,10 +7,18 @@
"build": "prisma generate && next build",
"start": "next start",
"lint": "eslint",
"postinstall": "prisma generate"
"postinstall": "prisma generate",
"build:worker": "opennextjs-cloudflare build",
"dev:worker": "wrangler dev",
"deploy": "bun run build:worker && wrangler deploy",
"d1:migrate": "wrangler d1 migrations apply quit-smoking-db --local",
"d1:migrate:prod": "wrangler d1 migrations apply quit-smoking-db --remote"
},
"dependencies": {
"@opennextjs/cloudflare": "^1.1.1",
"@prisma/adapter-d1": "^5.22.0",
"@prisma/client": "5",
"@workos-inc/authkit-nextjs": "^0.16.0",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
@ -33,6 +41,7 @@
"tailwind-merge": "^3.4.0"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20250121.0",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
@ -41,7 +50,8 @@
"eslint-config-next": "16.1.4",
"tailwindcss": "^4",
"tw-animate-css": "^1.4.0",
"typescript": "^5"
"typescript": "^5",
"wrangler": "^4"
},
"ignoreScripts": [
"sharp",
@ -51,4 +61,4 @@
"sharp",
"unrs-resolver"
]
}
}

View File

@ -1,14 +0,0 @@
// This file was generated by Prisma, and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import "dotenv/config";
import { defineConfig } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: process.env["DATABASE_URL"],
},
});

82
prisma/d1_schema.sql Normal file
View File

@ -0,0 +1,82 @@
-- CreateTable
CREATE TABLE "UserPreferences" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"substance" TEXT NOT NULL DEFAULT 'nicotine',
"trackingStartDate" TEXT,
"hasCompletedSetup" BOOLEAN NOT NULL DEFAULT false,
"dailyGoal" INTEGER,
"userName" TEXT,
"userAge" INTEGER,
"religion" TEXT,
"lastNicotineUsageTime" TEXT,
"lastWeedUsageTime" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"quitPlanJson" TEXT
);
-- CreateTable
CREATE TABLE "UsageEntry" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"date" TEXT NOT NULL,
"count" INTEGER NOT NULL,
"substance" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "UsageEntry_userId_fkey" FOREIGN KEY ("userId") REFERENCES "UserPreferences" ("userId") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "Achievement" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"badgeId" TEXT NOT NULL,
"unlockedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"substance" TEXT NOT NULL,
CONSTRAINT "Achievement_userId_fkey" FOREIGN KEY ("userId") REFERENCES "UserPreferences" ("userId") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "ReminderSettings" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"enabled" BOOLEAN NOT NULL DEFAULT false,
"reminderTime" TEXT NOT NULL DEFAULT '09:00',
"lastNotifiedDate" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ReminderSettings_userId_fkey" FOREIGN KEY ("userId") REFERENCES "UserPreferences" ("userId") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "SavingsConfig" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"costPerUnit" REAL NOT NULL,
"unitsPerDay" REAL NOT NULL,
"savingsGoal" REAL,
"goalName" TEXT,
"currency" TEXT NOT NULL DEFAULT 'USD',
"substance" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "SavingsConfig_userId_fkey" FOREIGN KEY ("userId") REFERENCES "UserPreferences" ("userId") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "UserPreferences_userId_key" ON "UserPreferences"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "UsageEntry_userId_date_substance_key" ON "UsageEntry"("userId", "date", "substance");
-- CreateIndex
CREATE UNIQUE INDEX "Achievement_userId_badgeId_substance_key" ON "Achievement"("userId", "badgeId", "substance");
-- CreateIndex
CREATE UNIQUE INDEX "ReminderSettings_userId_key" ON "ReminderSettings"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "SavingsConfig_userId_key" ON "SavingsConfig"("userId");

View File

@ -2,14 +2,16 @@
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}
datasource db {
provider = "sqlite"
url = "file:./dev.db"
url = env("DATABASE_URL")
}
model UserPreferences {
id String @id @default(cuid())
userId String @unique

View File

@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { getPrismaWithD1 } from '@/lib/prisma';
import { getSession } from '@/lib/session';
export async function GET() {
@ -9,6 +9,7 @@ export async function GET() {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const prisma = await getPrismaWithD1();
const achievements = await prisma.achievement.findMany({
where: { userId: session.user.id },
orderBy: { unlockedAt: 'desc' },
@ -34,13 +35,15 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const body = await request.json() as { badgeId?: string; substance?: string };
const { badgeId, substance } = body;
if (!badgeId || !substance) {
return NextResponse.json({ error: 'Missing badgeId or substance' }, { status: 400 });
}
const prisma = await getPrismaWithD1();
// Check if achievement already exists
const existing = await prisma.achievement.findUnique({
where: {

View File

@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { getPrismaWithD1 } from '@/lib/prisma';
import { getSession } from '@/lib/session';
export async function GET() {
@ -9,6 +9,7 @@ export async function GET() {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const prisma = await getPrismaWithD1();
const preferences = await prisma.userPreferences.findUnique({
where: { userId: session.user.id },
});
@ -50,7 +51,18 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const body = await request.json() as {
substance?: string;
trackingStartDate?: string;
hasCompletedSetup?: boolean;
dailyGoal?: number;
quitPlan?: unknown;
userName?: string;
userAge?: number;
religion?: string;
lastNicotineUsageTime?: string;
lastWeedUsageTime?: string;
};
const {
substance,
trackingStartDate,
@ -64,6 +76,7 @@ export async function POST(request: NextRequest) {
lastWeedUsageTime
} = body;
const prisma = await getPrismaWithD1();
const preferences = await prisma.userPreferences.upsert({
where: { userId: session.user.id },
update: {

View File

@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { getPrismaWithD1 } from '@/lib/prisma';
import { getSession } from '@/lib/session';
export async function GET() {
@ -9,6 +9,7 @@ export async function GET() {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const prisma = await getPrismaWithD1();
const settings = await prisma.reminderSettings.findUnique({
where: { userId: session.user.id },
});
@ -37,9 +38,10 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const body = await request.json() as { enabled?: boolean; reminderTime?: string };
const { enabled, reminderTime } = body;
const prisma = await getPrismaWithD1();
const settings = await prisma.reminderSettings.upsert({
where: { userId: session.user.id },
update: {

View File

@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { getPrismaWithD1 } from '@/lib/prisma';
import { getSession } from '@/lib/session';
export async function GET() {
@ -9,6 +9,7 @@ export async function GET() {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const prisma = await getPrismaWithD1();
const config = await prisma.savingsConfig.findUnique({
where: { userId: session.user.id },
});
@ -38,13 +39,21 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const body = await request.json() as {
costPerUnit?: number;
unitsPerDay?: number;
savingsGoal?: number;
goalName?: string;
currency?: string;
substance?: string;
};
const { costPerUnit, unitsPerDay, savingsGoal, goalName, currency, substance } = body;
if (costPerUnit === undefined || unitsPerDay === undefined || !substance) {
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
}
const prisma = await getPrismaWithD1();
const config = await prisma.savingsConfig.upsert({
where: { userId: session.user.id },
update: {

View File

@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { getPrismaWithD1 } from '@/lib/prisma';
import { getSession } from '@/lib/session';
export async function GET() {
@ -9,6 +9,7 @@ export async function GET() {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const prisma = await getPrismaWithD1();
const entries = await prisma.usageEntry.findMany({
where: { userId: session.user.id },
orderBy: { date: 'desc' },
@ -34,16 +35,22 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const body = await request.json() as { date?: string; count?: number; substance?: string };
const { date, count, substance } = body;
if (!date || count === undefined || !substance) {
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
}
const prisma = await getPrismaWithD1();
// Upsert: add to existing count or create new entry
const existing = await prisma.usageEntry.findUnique({
where: {
userId_date_substance: {
userId: session.user.id,
date,
substance,
date: date,
substance: substance,
},
},
});
@ -62,9 +69,9 @@ export async function POST(request: NextRequest) {
const created = await prisma.usageEntry.create({
data: {
userId: session.user.id,
date,
count,
substance,
date: date,
count: count,
substance: substance,
},
});
return NextResponse.json({
@ -86,24 +93,30 @@ export async function PUT(request: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const body = await request.json() as { date?: string; count?: number; substance?: string };
const { date, count, substance } = body;
if (!date || count === undefined || !substance) {
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
}
const prisma = await getPrismaWithD1();
// Set the exact count (replace, not add)
const entry = await prisma.usageEntry.upsert({
where: {
userId_date_substance: {
userId: session.user.id,
date,
substance,
date: date,
substance: substance,
},
},
update: { count },
update: { count: count },
create: {
userId: session.user.id,
date,
count,
substance,
date: date,
count: count,
substance: substance,
},
});
@ -133,6 +146,8 @@ export async function DELETE(request: NextRequest) {
return NextResponse.json({ error: 'Missing date or substance' }, { status: 400 });
}
const prisma = await getPrismaWithD1();
await prisma.usageEntry.deleteMany({
where: {
userId: session.user.id,

View File

@ -2,40 +2,63 @@
import { useState, useEffect, useCallback } from "react";
import {
getUserData,
saveUserData,
getUsageLogs,
addUsageLog,
getRecentLogs,
calculateStats,
getQuitPlan,
generateQuitPlan,
getCurrentWeek,
canGeneratePlan,
needsCheckIn as checkNeedsCheckIn,
getPreferences,
savePreferences,
getUsageData,
saveUsageEntry,
getWeeklyData,
calculateWeeklyAverage,
generateQuitPlan as generateQuitPlanFn,
hasOneWeekOfData,
fetchPreferences,
fetchUsageData,
UserPreferences,
UsageEntry,
QuitPlan,
} from "@/lib/storage";
import { UserData, UsageLog, QuitPlan, SubstanceType } from "@/types";
type SubstanceType = 'nicotine' | 'weed';
interface UserData {
substanceType: SubstanceType;
stayLoggedIn: boolean;
onboardingComplete: boolean;
}
export function useUserData() {
const [userData, setUserData] = useState<UserData | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setUserData(getUserData());
setIsLoading(false);
const loadData = async () => {
const prefs = await fetchPreferences();
setUserData({
substanceType: prefs.substance,
stayLoggedIn: true,
onboardingComplete: prefs.hasCompletedSetup,
});
setIsLoading(false);
};
loadData();
}, []);
const updateUserData = useCallback((data: Partial<UserData>) => {
const updated = saveUserData(data);
setUserData(updated);
const prefs = getPreferences();
const updated: UserPreferences = {
...prefs,
substance: data.substanceType || prefs.substance,
hasCompletedSetup: data.onboardingComplete ?? prefs.hasCompletedSetup,
};
savePreferences(updated);
setUserData((prev) => (prev ? { ...prev, ...data } : null));
return updated;
}, []);
const completeOnboarding = useCallback(
(substanceType: SubstanceType, stayLoggedIn: boolean) => {
(substanceType: SubstanceType, _stayLoggedIn: boolean) => {
return updateUserData({
substanceType,
stayLoggedIn,
stayLoggedIn: true,
onboardingComplete: true,
});
},
@ -52,32 +75,52 @@ export function useUserData() {
}
export function useUsageLogs() {
const [logs, setLogs] = useState<UsageLog[]>([]);
const [logs, setLogs] = useState<UsageEntry[]>([]);
const [isLoading, setIsLoading] = useState(true);
const refreshLogs = useCallback(() => {
setLogs(getUsageLogs());
const refreshLogs = useCallback(async () => {
const data = await fetchUsageData();
setLogs(data);
}, []);
useEffect(() => {
refreshLogs();
setIsLoading(false);
refreshLogs().then(() => setIsLoading(false));
}, [refreshLogs]);
const logUsage = useCallback(
(puffs: number, date?: string) => {
const newLog = addUsageLog(puffs, date);
const prefs = getPreferences();
const logDate = date || new Date().toISOString().split("T")[0];
const newEntry: UsageEntry = {
date: logDate,
count: puffs,
substance: prefs.substance,
};
saveUsageEntry(newEntry);
refreshLogs();
return newLog;
return newEntry;
},
[refreshLogs]
);
const getRecent = useCallback((days: number = 7) => {
return getRecentLogs(days);
const data = getUsageData();
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - days);
return data.filter((e) => new Date(e.date) >= cutoff);
}, []);
const stats = calculateStats(getRecentLogs(7));
const calculateStats = (entries: UsageEntry[]) => {
if (entries.length === 0) {
return { totalPuffs: 0, averagePuffs: 0, daysTracked: 0 };
}
const totalPuffs = entries.reduce((sum, e) => sum + e.count, 0);
const daysTracked = new Set(entries.map((e) => e.date)).size;
const averagePuffs = Math.round(totalPuffs / daysTracked);
return { totalPuffs, averagePuffs, daysTracked };
};
const stats = calculateStats(getRecent(7));
return {
logs,
@ -93,30 +136,44 @@ export function useQuitPlan() {
const [plan, setPlan] = useState<QuitPlan | null>(null);
const [isLoading, setIsLoading] = useState(true);
const refreshPlan = useCallback(() => {
setPlan(getQuitPlan());
const refreshPlan = useCallback(async () => {
const prefs = await fetchPreferences();
setPlan(prefs.quitPlan);
}, []);
useEffect(() => {
refreshPlan();
setIsLoading(false);
refreshPlan().then(() => setIsLoading(false));
}, [refreshPlan]);
const createPlan = useCallback(() => {
const recentLogs = getRecentLogs(7);
const { averagePuffs } = calculateStats(recentLogs);
if (averagePuffs === 0) {
const prefs = getPreferences();
const avgPuffs = calculateWeeklyAverage(prefs.substance);
if (avgPuffs === 0) {
return null;
}
const newPlan = generateQuitPlan(averagePuffs);
const newPlan = generateQuitPlanFn(prefs.substance);
const updatedPrefs = { ...prefs, quitPlan: newPlan };
savePreferences(updatedPrefs);
setPlan(newPlan);
return newPlan;
}, []);
const getCurrentWeek = (quitPlan: QuitPlan): number => {
const startDate = new Date(quitPlan.startDate);
const today = new Date();
const weekNumber = Math.floor(
(today.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24 * 7)
);
return Math.min(weekNumber, quitPlan.weeklyTargets.length - 1);
};
const currentWeek = plan ? getCurrentWeek(plan) : 0;
const { canGenerate, daysTracked } = canGeneratePlan();
const prefs = getPreferences();
const canGenerate = hasOneWeekOfData(prefs.substance);
const weeklyData = getWeeklyData(prefs.substance);
const daysTracked = new Set(weeklyData.map((e) => e.date)).size;
return {
plan,
@ -133,7 +190,11 @@ export function useCheckIn() {
const [needsCheckIn, setNeedsCheckIn] = useState(false);
useEffect(() => {
setNeedsCheckIn(checkNeedsCheckIn());
// Check if user needs to check in today
const today = new Date().toISOString().split("T")[0];
const data = getUsageData();
const todayEntry = data.find((e) => e.date === today);
setNeedsCheckIn(!todayEntry);
}, []);
const markCheckedIn = useCallback(() => {

View File

@ -1,9 +1,39 @@
import { PrismaClient } from '@prisma/client';
import { PrismaD1 } from '@prisma/adapter-d1';
// For local development fallback
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
/**
* Get Prisma client for Cloudflare Workers with D1.
* This should be called in each request handler to get a fresh client with D1 binding.
*/
export async function getPrismaWithD1(): Promise<PrismaClient> {
// Try to get Cloudflare context (only works in Workers environment)
try {
const { getCloudflareContext } = await import('@opennextjs/cloudflare');
const { env } = await getCloudflareContext();
if (env?.DB) {
const adapter = new PrismaD1(env.DB);
return new PrismaClient({ adapter });
}
} catch {
// Not in Cloudflare Workers environment, fall back to local client
}
// Fallback for local development
if (!globalForPrisma.prisma) {
globalForPrisma.prisma = new PrismaClient();
}
return globalForPrisma.prisma;
}
// Legacy synchronous export for backward compatibility in local dev
// NOTE: This does NOT work with D1 in production - use getPrismaWithD1() instead
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') {

View File

@ -134,7 +134,7 @@ export async function fetchPreferences(): Promise<UserPreferences> {
console.error('Failed to fetch preferences');
return defaultPreferences;
}
const data = await response.json();
const data = await response.json() as UserPreferences;
preferencesCache = data;
return data;
} catch (error) {
@ -166,7 +166,7 @@ export async function fetchUsageData(): Promise<UsageEntry[]> {
console.error('Failed to fetch usage data');
return [];
}
const data = await response.json();
const data = await response.json() as UsageEntry[];
usageDataCache = data;
return data;
} catch (error) {
@ -226,7 +226,7 @@ export async function fetchAchievements(): Promise<Achievement[]> {
try {
const response = await fetch('/api/achievements');
if (!response.ok) return [];
const data = await response.json();
const data = await response.json() as Achievement[];
achievementsCache = data;
return data;
} catch (error) {
@ -246,7 +246,7 @@ export async function unlockAchievement(
body: JSON.stringify({ badgeId, substance }),
});
if (response.ok) {
const data = await response.json();
const data = await response.json() as { badgeId: string; unlockedAt: string; substance: 'nicotine' | 'weed' | 'both'; alreadyUnlocked?: boolean };
achievementsCache = null; // Invalidate cache
return {
achievement: {
@ -275,7 +275,7 @@ export async function fetchReminderSettings(): Promise<ReminderSettings> {
try {
const response = await fetch('/api/reminders');
if (!response.ok) return { enabled: false, reminderTime: '09:00' };
const data = await response.json();
const data = await response.json() as ReminderSettings;
reminderSettingsCache = data;
return data;
} catch (error) {
@ -310,7 +310,7 @@ export async function fetchSavingsConfig(): Promise<SavingsConfig | null> {
try {
const response = await fetch('/api/savings');
if (!response.ok) return null;
const data = await response.json();
const data = await response.json() as SavingsConfig | null;
savingsConfigCache = data;
return data;
} catch (error) {

39
wrangler.jsonc Normal file
View File

@ -0,0 +1,39 @@
{
// Worker name - change this to your preferred name
"name": "quit-smoking-app",
// Use the OpenNext preset for Next.js on Cloudflare Workers
"main": ".open-next/worker.js",
// Compatibility settings for Node.js APIs
"compatibility_date": "2024-09-23",
"compatibility_flags": [
"nodejs_compat"
],
// Set up route matching for static assets
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
},
// D1 Database binding
"d1_databases": [
{
"binding": "DB",
"database_name": "quit-smoking-db",
"database_id": "1fca511a-cf91-4fde-854b-561ed92abfa8"
}
],
// Environment variables
"vars": {
"NODE_ENV": "production"
},
// Custom domain
"routes": [
{
"pattern": "quittraq.com/*",
"zone_name": "quittraq.com"
},
{
"pattern": "www.quittraq.com/*",
"zone_name": "quittraq.com"
}
]
}