= { auto: 'dark', dark: 'light', light: 'auto' };
+ const t = next[currentTheme()];
+ setTheme(t);
+ return t;
+}
diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte
new file mode 100644
index 0000000..719bef5
--- /dev/null
+++ b/web/src/routes/+layout.svelte
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+ {@render children()}
+
+
+
+
diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte
new file mode 100644
index 0000000..dab926a
--- /dev/null
+++ b/web/src/routes/+page.svelte
@@ -0,0 +1,72 @@
+
+
+potluck
+a token melting pot for hackclubbers.
+
+{#if err}
+ not signed in. visit /api/dev/login?email=you@example.com
+{:else if user}
+
+ your stake
+ ${bal?.balance_usd ?? '—'}
+ signed in as {user.email}
+
+{:else}
+ loading…
+{/if}
+
+
diff --git a/web/src/routes/api/[...path]/+server.ts b/web/src/routes/api/[...path]/+server.ts
new file mode 100644
index 0000000..5086433
--- /dev/null
+++ b/web/src/routes/api/[...path]/+server.ts
@@ -0,0 +1,40 @@
+/**
+ * /api/* proxy.
+ *
+ * The Cloudflare Worker (this server runtime) is intentionally dumb: take
+ * the inbound request, swap in BACKEND_URL, forward it. No auth, no
+ * rewriting, no caching. All real work happens in the Go backend.
+ *
+ * Streaming is preserved because we hand the original body and ReadableStream
+ * straight through.
+ */
+
+import type { RequestHandler } from './$types';
+
+const proxy: RequestHandler = async ({ request, url, platform }) => {
+ // SvelteKit's $env is overkill for one variable; read straight from
+ // platform.env in production, fall back for local dev.
+ const backend =
+ (platform?.env as { BACKEND_URL?: string } | undefined)?.BACKEND_URL ??
+ 'http://localhost:8080';
+
+ const target = new URL(url.pathname + url.search, backend);
+
+ const init: RequestInit = {
+ method: request.method,
+ headers: request.headers,
+ body:
+ request.method === 'GET' || request.method === 'HEAD' ? undefined : (request.body as never),
+ redirect: 'manual'
+ };
+ // Streaming bodies need duplex on fetch().
+ if (init.body) (init as { duplex?: string }).duplex = 'half';
+
+ return fetch(target, init);
+};
+
+export const GET = proxy;
+export const POST = proxy;
+export const PUT = proxy;
+export const PATCH = proxy;
+export const DELETE = proxy;
diff --git a/web/src/routes/chat/+page.svelte b/web/src/routes/chat/+page.svelte
new file mode 100644
index 0000000..18d9699
--- /dev/null
+++ b/web/src/routes/chat/+page.svelte
@@ -0,0 +1,94 @@
+
+
+
+ chat
+
+
+
+{#if loading}
+ loading…
+{:else if err}
+ {err}
+{:else if conversations.length === 0}
+ no conversations yet. create one to get started.
+{:else}
+
+ {#each conversations as c (c.id)}
+ -
+ {c.title || 'untitled'}
+ {new Date(c.updated_at * 1000).toLocaleString()}
+
+ {/each}
+
+{/if}
+
+
diff --git a/web/svelte.config.js b/web/svelte.config.js
new file mode 100644
index 0000000..79cdfaf
--- /dev/null
+++ b/web/svelte.config.js
@@ -0,0 +1,18 @@
+import adapter from '@sveltejs/adapter-cloudflare';
+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
+
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+ preprocess: vitePreprocess(),
+ kit: {
+ adapter: adapter(),
+ alias: {
+ $lib: 'src/lib'
+ }
+ },
+ compilerOptions: {
+ runes: true
+ }
+};
+
+export default config;
diff --git a/web/tsconfig.json b/web/tsconfig.json
new file mode 100644
index 0000000..4344710
--- /dev/null
+++ b/web/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "./.svelte-kit/tsconfig.json",
+ "compilerOptions": {
+ "allowJs": true,
+ "checkJs": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "sourceMap": true,
+ "strict": true,
+ "moduleResolution": "bundler"
+ }
+}
diff --git a/web/vite.config.ts b/web/vite.config.ts
new file mode 100644
index 0000000..2a0ab80
--- /dev/null
+++ b/web/vite.config.ts
@@ -0,0 +1,15 @@
+import { sveltekit } from '@sveltejs/kit/vite';
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ plugins: [sveltekit()],
+ server: {
+ port: 3000,
+ proxy: {
+ '/api': {
+ target: 'http://localhost:8080',
+ changeOrigin: false
+ }
+ }
+ }
+});
diff --git a/web/wrangler.toml b/web/wrangler.toml
new file mode 100644
index 0000000..8f37cac
--- /dev/null
+++ b/web/wrangler.toml
@@ -0,0 +1,10 @@
+name = "potluck"
+main = ".svelte-kit/cloudflare/_worker.js"
+compatibility_date = "2025-04-01"
+compatibility_flags = ["nodejs_compat"]
+
+[site]
+bucket = ".svelte-kit/cloudflare"
+
+[vars]
+BACKEND_URL = "https://potluck-api.dunkirk.sh"