# Supabase Gallery Sync Reference ## Naming Use `wg_` for project-owned Supabase objects. - Gallery table: `wg_gallery_photos` - Sync state table: `wg_gallery_sync_state` - Collections tables: `wg_photo_collections`, `wg_photo_collection_items` - User tables: `wg_app_users`, `wg_user_favorites`, `wg_user_settings` - Example trigger/function names: `set_wg_gallery_photos_updated_at`, `set_wg_updated_at` Do not introduce unprefixed replacements such as `gallery_photos`. ## Files - `supabase/migrations/20260530000000_create_wg_gallery_tables.sql`: table/RLS/index/trigger migration. - `lib/supabase/service-role.ts`: server-only Supabase service-role client. - `supabase/migrations/20260601000000_extend_gallery_pick_collections_users.sql`: picked-photo, OSS metadata, collections, and user/favorite/settings schema extension. - `server/api/unsplash.ts`: official Unsplash API fetch and response validation. - `server/admin/gallery-sync.ts`: reusable sync implementation for Next route handlers. - `server/admin/gallery-pick.ts`: admin pick/unpick implementation and OSS mirror call. - `server/storage/aliyun-oss.ts`: server-only Aliyun OSS PUT Object helper. - `app/api/admin/gallery-photos/route.ts`: protected admin list + pick/unpick API. - `app/api/admin/photo-collections/route.ts`: protected admin collection list/create API. - `app/api/admin/photo-collections/[id]/route.ts`: protected admin collection read/update/delete API. - `app/api/v1/gallery/photos/route.ts`: public picked photos API. - `app/api/v1/gallery/collections/route.ts`: public published collections API. - `app/api/v1/gallery/collections/[slug]/route.ts`: public published collection detail API. - `app/api/cron/gallery-photos/route.ts`: protected HTTP trigger, guarded by `CRON_SECRET`. - `scripts/sync-gallery-photos.mjs`: one-shot Node script for server cron/process managers. ## Environment Required for sync: - `NEXT_PUBLIC_SUPABASE_URL` - `SUPABASE_SERVICE_ROLE_KEY` - `UNSPLASH_ACCESS_KEY` Required for OSS mirroring when an admin marks a photo as picked: - `OSS_REGION` - `OSS_ACCESS_KEY_ID` - `OSS_ACCESS_KEY_SECRET` - `OSS_BUCKET` - `OSS_PUBLIC_URL` or `OSS_PUBLIC_BASE_URL` Optional for OSS: - `OSS_ENDPOINT` to override the region-derived endpoint Required for HTTP trigger: - `CRON_SECRET` Never print secret values. It is fine to report whether each variable is set. ## Scheduler This project is not intended for Vercel deployment. Do not add `vercel.json` cron config unless the deployment target changes. Preferred server cron: ```bash */10 * * * * cd /Users/luoyangwei/Documents/workspace/wallora.top/searcher && pnpm sync:gallery ``` The script runs once and exits. That makes it suitable for crontab, PM2 cron, Docker scheduled jobs, or an external scheduler. ## Sync Behavior Source endpoint: ```txt https://api.unsplash.com/topics/wallpapers/photos?page=&per_page=20 ``` Use the official Unsplash API with `Authorization: Client-ID `. Do not rely on `unsplash.com/napi/...`; it is a website-internal endpoint and can return `401`, anti-bot HTML, or other non-contract responses. Workflow: 1. Read `wg_gallery_sync_state` for source `unsplash-wallpapers`. 2. Fetch the current page from the official Unsplash API with `per_page = 20`. 3. Require JSON and validate that the response is an array of photo-like objects. 4. Map every item into `wg_gallery_photos`. 5. Populate `color_family` from the Unsplash hex color so admin color filters work. 6. Upsert with `onConflict: "unsplash_id"` and `ignoreDuplicates: true`. 7. Update `wg_gallery_sync_state` with next page, seen count, inserted count, timestamp, and last error. If Unsplash returns an error, anti-bot HTML, or any non-JSON response, record the error in `wg_gallery_sync_state.last_error` and do not write photo rows. ## Picked Photos - Raw gallery rows are for admin curation. - Public consumers should query picked rows only through `/api/v1/gallery/photos`. - RLS should expose `wg_gallery_photos` to `anon`/`authenticated` only when `is_picked = true`. - Admin pick/unpick operations run through server-only code using the Supabase service-role key. - Picking a photo attempts to mirror the selected Unsplash image into Aliyun OSS and stores `oss_bucket`, `oss_object_key`, `oss_url`, `oss_synced_at`, and `oss_sync_error`. - If OSS is missing or upload fails, the photo can still be marked picked, but `oss_sync_error` should be visible to admin users. ## Collections - Collections are admin-managed under `/admin/collections`. - Admin APIs are session-protected and run with the Supabase service-role client. - Collection items must reference picked photos only. - Published collections are exposed publicly; draft collections are not returned by public APIs. - Public collection detail uses `page` and `pageSize` for waterfall-friendly pagination of collection photos. - Public gallery photo detail lives at `/api/v1/gallery/photos/[id]`; it is picked-only and supports authenticated favorite/unfavorite shortcuts. ## App Users - App users are separate from admin users and live in `wg_app_users`. - Email/password auth uses server-side password hashing and Bearer JWT issuance under `/api/v1/auth/*`. - Protected app-user routes live under `/api/v1/users/me/*` and must validate Bearer JWTs server-side. - Admin app-user review and status control live under `/admin/users` and `/api/admin/app-users/[id]`. - Favorites live in `wg_user_favorites` and should only reference picked photos. - Preferences and download preferences live in `wg_user_settings` as JSON objects. - `wg_app_users` has additive identity columns for future phone and WeChat login support. ## Verification Use these checks after touching sync code: ```bash node --check scripts/sync-gallery-photos.mjs pnpm format:check pnpm lint pnpm build ``` Only run `pnpm sync:gallery` when the user explicitly wants to write to the configured Supabase database.