106 lines
5.5 KiB
Markdown
106 lines
5.5 KiB
Markdown
# Deploying for free (Vercel + Supabase free tier)
|
|
|
|
This app runs 100% on free tiers: **Vercel Hobby** (Next.js hosting) + **Supabase Free**
|
|
(Postgres for metadata + Storage for the media files). No paid services are required.
|
|
The in-browser AI (object detection, faces, OCR) runs on the user's device for free; only the
|
|
optional Gemini generative edit needs a key (and can be left off).
|
|
|
|
> TL;DR — run the SQL once, push to GitHub, import into Vercel with **Root Directory = `apps/web`**,
|
|
> set the two `NEXT_PUBLIC_SUPABASE_*` env vars, deploy. Everything (photos, videos, tags, versions,
|
|
> comments, albums, edited copies) then persists to your Supabase project.
|
|
|
|
---
|
|
|
|
## 1. Supabase (database + file storage)
|
|
|
|
1. Create a project at <https://supabase.com> (free tier).
|
|
2. Open **SQL Editor → New query**, paste all of [`docs/supabase-setup.sql`](./supabase-setup.sql),
|
|
and **Run**. This creates the metadata tables, the demo RLS policies, **and the public `media`
|
|
storage bucket** (the bucket is what makes uploaded/edited/captured files survive a reload —
|
|
without it the app silently falls back to non-durable URLs).
|
|
3. Confirm **Storage** now lists a bucket named `media` marked **Public**.
|
|
4. **Project Settings → API** → copy:
|
|
- **Project URL** → `NEXT_PUBLIC_SUPABASE_URL`
|
|
- **anon public** key → `NEXT_PUBLIC_SUPABASE_ANON_KEY`
|
|
(Never use the `service_role` key in the client — it stays server-side only, if used at all.)
|
|
|
|
> Free-tier note: a Supabase project **pauses after ~7 days idle**. Open the dashboard to unpause
|
|
> before a demo. Storage free tier is 1 GB, Postgres 500 MB — plenty for a demo library.
|
|
|
|
## 2. Push the repo to GitHub
|
|
|
|
Commit everything **including `pnpm-lock.yaml`** at the repo root (Vercel uses it to detect pnpm and
|
|
install the whole workspace). Do not commit `.env.local` (it's gitignored).
|
|
|
|
## 3. Vercel
|
|
|
|
1. <https://vercel.com> → **Add New → Project** → import your GitHub repo.
|
|
2. **Configure the project:**
|
|
| Setting | Value |
|
|
|---|---|
|
|
| **Root Directory** | `apps/web` |
|
|
| **Framework Preset** | Next.js (auto-detected) |
|
|
| **Install Command** | `pnpm install` (default) |
|
|
| **Build Command** | `next build` (default) |
|
|
| **Output** | `.next` (default) |
|
|
| **Node.js Version** | 20.x |
|
|
|
|
> No separate SDK build step is needed: `@photo-gallery/sdk` is consumed from TypeScript source
|
|
> via Next's `transpilePackages`, so `pnpm install` (which links the workspace) is enough.
|
|
|
|
3. **Environment Variables** (Project → Settings → Environment Variables), for Production + Preview:
|
|
|
|
**Client (safe to expose — required for persistence):**
|
|
| Name | Value |
|
|
|---|---|
|
|
| `NEXT_PUBLIC_SUPABASE_URL` | your Project URL |
|
|
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | your anon public key |
|
|
| `NEXT_PUBLIC_SUPABASE_BUCKET` | `media` *(optional; default is `media`)* |
|
|
|
|
**Server-only (optional — do NOT prefix with `NEXT_PUBLIC`):**
|
|
| Name | Value |
|
|
|---|---|
|
|
| `GEMINI_API_KEY` | a Gemini key *(only for the AI generative-edit button)* |
|
|
| `GEMINI_IMAGE_MODEL` | `gemini-2.5-flash-image` *(optional)* |
|
|
|
|
Without the Supabase vars the app still runs, but on browser-local storage only (data won't sync
|
|
across devices). Without `GEMINI_API_KEY` the generative-edit route returns 503 while all the free
|
|
in-browser AI keeps working.
|
|
|
|
Optional theming/feature flags (`NEXT_PUBLIC_APG_*`) are documented in [`docs/ENV.md`](./ENV.md).
|
|
|
|
4. **Deploy.**
|
|
|
|
## 4. Verify after the first deploy
|
|
|
|
- Response headers include a per-request `content-security-policy` with a `nonce-…` (confirms the
|
|
nonce CSP middleware works on Vercel — expected; the app renders dynamically, not as a static export).
|
|
- Import a photo → its `src` becomes a `https://<project>.supabase.co/...` URL → **hard-reload**: it
|
|
still shows. Same for a camera capture and an edited copy.
|
|
- Edit a photo twice → the Info panel shows **Version 3**; reload → history + comments persist.
|
|
- The **Objects** sidebar section fills in as on-device detection tags photos.
|
|
|
|
## What persists to the database
|
|
|
|
Everything is stored as JSONB per item (`gallery_media.data`) + binaries in Storage, so a reload
|
|
restores it all: **tags, detected objects/labels, faces, versions[] (full edit history + audit log),
|
|
comments[], albums, favorites, edits, and Save-as-Copy items** (each copy is a new row with its own
|
|
2-entry history). System/smart albums (incl. the auto **object albums**) are regenerated on load, so
|
|
they're intentionally not stored.
|
|
|
|
## Gotchas / limits (free tier)
|
|
|
|
- **Gemini AI edit + Vercel body limit:** Vercel Hobby caps request bodies at ~4.5 MB. The AI-edit
|
|
route is capped accordingly and large images are downscaled client-side first; very large images
|
|
may still be rejected. The free in-browser AI is unaffected.
|
|
- **Single-user demo:** the Supabase adapter does a full-state sync (no auth). For multi-user, add
|
|
Supabase Auth and scope rows per user before going to production (the RLS policies are open for the
|
|
demo — tighten them with `auth.uid()`).
|
|
- **In-browser video export** (trim/overlays/watermark/etc.) requires the source video to be readable
|
|
for canvas capture. Same-origin (Supabase Storage with CORS, which is on by default) works; a
|
|
cross-origin host without CORS headers can't be exported in-browser (the editor now surfaces a clear
|
|
error instead of failing silently).
|
|
- **Heavy AI libs** (TensorFlow.js / face-api / tesseract / transformers / imgly) are dynamically
|
|
imported on the client only — they don't bloat the server bundle, but first analysis downloads model
|
|
weights (cached afterwards).
|