A personal photo catalog + AI chatbot site. Drop media files into a folder, get a gallery. Chat with a funny AI assistant powered by Ollama Cloud. Gated behind a simple passcode.
- Photo catalog — media stored in Supabase Storage, metadata in Supabase Postgres. Displays them in a responsive masonry grid with a fullscreen lightbox (keyboard navigation, video playback).
- AI chatbot — floating chat widget powered by Ollama Cloud. Streaming responses with a custom funny personality. Conversation history saved to Supabase.
- Passcode auth — single hardcoded passcode (
613) with a signed HttpOnly cookie. No user accounts. - Shareable photos — every photo has a public
/p/[id]URL with full Open Graph / Twitter Card metadata, plus an oEmbed endpoint for rich previews on Discord, Slack, Mastodon, Notion, etc. - Embeddable viewer — drop a Voyle photo into any site with
<iframe src="https://your-host/embed/[id]">.
npm installCopy .env.example to .env.local and fill in your credentials:
cp .env.example .env.localRequired variables (see .env.example for the full list):
SUPABASE_URL,SUPABASE_KEY,SUPABASE_SERVICE_ROLE_KEY— Supabase project (server-side)NEXT_PUBLIC_SUPABASE_URL,NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY— Supabase (browser client)AUTH_CODE— the passcode to access the site (default:613)AUTH_SECRET— random string for signing cookies (change this!)OLLAMA_BASE_URL,OLLAMA_API_KEY,OLLAMA_MODEL— Ollama Cloud chatbotSUPABASE_STORAGE_BUCKET— Supabase Storage bucket name for media files (defaults tomedia)IMAGE_GEN_API_URL,IMAGE_GEN_API_KEY— image generation Worker
Run the SQL files in supabase/ via the Supabase SQL Editor (in order):
users.sql,media.sql,system_prompt.sql,lockdown.sql,login_attempts.sql,chat.sql
npm run devOpen http://localhost:3000. You'll be redirected to /login. Enter the passcode (613 by default).
Edit src/lib/prompts.ts to change the system prompt and personality of the chatbot.
src/
app/
api/
auth/route.ts # POST passcode → set cookie
auth/logout/route.ts # POST → clear cookie
auth/me/route.ts # GET → current user
chat/route.ts # POST → stream Ollama Cloud response
generate/route.ts # POST → image generation (Workers AI)
media/route.ts # GET list / POST upload to Supabase Storage
oembed/route.ts # oEmbed endpoint
og/route.ts # dynamic OG image
(app)/ # authenticated app routes
page.tsx # catalog page (grid)
channel/ # "My Channel" + browse
channels/ # all channels
chat/ # full-page chat
generate/ # image generation UI
muzic/ # music player (uploaded tracks)
shorts/ # shorts view
spencer/ # spencer view
embed/[id]/page.tsx # embeddable iframe viewer
p/[id]/page.tsx # public share URL (OG metadata)
login/page.tsx # passcode entry page
locked/page.tsx # lockdown page
components/ # UI components
lib/
auth.ts # HMAC token sign/verify
chat-store.ts # Supabase-backed chat persistence
channel.ts # channel attribution (Supabase)
media.ts # media library (Supabase Postgres + Storage)
ollama.ts # Ollama Cloud streaming client
prompts.ts # chatbot system prompt (Supabase)
storage.ts # Supabase Storage client (upload, head, URL)
user.ts # current user lookup
proxy.ts # auth gate + lockdown (Next.js proxy)
utils/supabase/ # Supabase SSR clients
supabase/ # SQL table definitions + RLS policies
cloudflare/worker.js # standalone image-gen Worker (Workers AI)
wrangler.jsonc # Cloudflare Workers config
open-next.config.ts # OpenNext adapter config
This machine runs a Techloq HTTPS content filter that intercepts all TLS
traffic with its own root CA (CN=techloq-CA). The CA lives in the Windows
trust store but not in Node.js's bundled CA list, so every fetch() /
Supabase / Ollama call from Node fails with:
unable to get local issuer certificate (UNABLE_TO_GET_ISSUER_CERT_LOCALLY)
A copy of the CA is committed at techloq-ca.pem, and npm run dev /
npm run start set NODE_EXTRA_CA_CERTS=./techloq-ca.pem via cross-env so
Node trusts the filter's certificates.
If the CA changes (e.g. after a Techloq update), regenerate the PEM:
$cert = Get-ChildItem Cert:\LocalMachine\Root | Where-Object { $_.Subject -like "*techloq-CA*" } | Select-Object -First 1
$pem = "-----BEGIN CERTIFICATE-----`n" + [Convert]::ToBase64String($cert.RawData, [Base64FormattingOptions]::InsertLineBreaks) + "`n-----END CERTIFICATE-----`n"
$pem | Set-Content -Path techloq-ca.pem -Encoding ASCII- Next.js 16 (App Router, Turbopack)
- TypeScript + Tailwind CSS v4
- Supabase (Postgres) — all metadata: users, media, chat history, prompts, lockdown
- Supabase Storage — media file storage (public bucket)
- Ollama Cloud (native
/api/chatstreaming API) - Cloudflare Workers AI — image generation (separate Worker)
The site deploys to Cloudflare Workers via @opennextjs/cloudflare.
npm run dev # Next.js dev server (Node.js)npm run preview # builds via OpenNext + runs in workerd locallyWindows note: OpenNext has known Windows issues for build/preview/deploy. If
npm run previewornpm run deployfails on Windows, run them inside WSL or via CI (GitHub Actions on Linux).
npm run deploy # builds + deploys to Cloudflare WorkersSet server-side secrets via wrangler secret put <NAME> or the dashboard:
SUPABASE_URL, SUPABASE_KEY, SUPABASE_SERVICE_ROLE_KEY, AUTH_SECRET,
AUTH_CODE, OLLAMA_*, IMAGE_GEN_*.
Set NEXT_PUBLIC_* vars as Workers Build build variables (they're inlined
into the client bundle at build time): NEXT_PUBLIC_SUPABASE_URL,
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY.
Media files are stored in Supabase Storage (public bucket media) — no
Cloudflare-specific storage bindings are needed.