Skip to content
live-miraclesPublic

About

Internal Web App for managing Studio booking, Equipment Inventory, and staff shift assignment

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Setu

Setu is an internal operations app built on Supabase and Vercel. The frontend is a Vercel-style static app; its API boundary is a Supabase Edge Function backed by Postgres.

It covers equipment and program requests, comments, roster scheduling, inventory, programs, home content, and user settings.

Free-tier deployment

This architecture can run on the free tiers of Supabase and Vercel for both the Setu and Setu Dev environments, within each provider's current quotas and terms. Supabase hosts the database, Storage, and Edge Functions; Apps Script runs the 10-minute comment-email worker, and Vercel only serves the static frontend. Supabase's current Free plan includes two active projects, 500,000 Edge Function invocations, 1 GB of file storage, and 500 MB of database storage per project. Free projects pause after 7 days with no API activity — each project's pause timer is independent, so pinging one does not keep the other awake.

.github/workflows/keep-alive.yml pings both the Setu and Setu Dev projects every 3 days to prevent this. It needs four repository secrets (Settings → Secrets and variables → Actions in GitHub), using the same URL/publishable key pair from each project's Project Settings → API Keys page (see step 1 below for where to find these):

  • SETU_SUPABASE_URL_PROD / SETU_SUPABASE_PUBLISHABLE_KEY_PROD — the Setu (production) project
  • SETU_SUPABASE_URL_DEV / SETU_SUPABASE_PUBLISHABLE_KEY_DEV — the Setu Dev project

Trigger a manual run from the Actions tab (Keep Supabase projects alive → Run workflow) to verify it after setup.

Comment email delivery depends on the Google Workspace account running the Apps Script worker. Apps Script has Workspace email-recipient and execution quotas, so this path is intended for Setu's low-volume internal notifications.

See the providers' current plan details before relying on these numbers: Supabase pricing, Vercel pricing, and Resend pricing.

Architecture

Browser → Vercel static app → Supabase Auth
                           ├→ Edge Function: api
                           ├→ Postgres
                           └→ Storage
  • frontend/ contains the browser application.
  • supabase/migrations/ contains the database schema.
  • supabase/functions/api/ contains the authenticated API boundary.
  • shared/types.d.ts contains the frontend/API contract.

Requirements

  • Node.js 22 or newer
  • A Supabase account and a development project
  • A Google OAuth client configured for that Supabase project
  • Git

Docker is not required for the recommended hosted-development workflow. The Supabase CLI is installed as a project dependency by npm install.

Start a new developer environment

1. Install the project

git clone <repository-url>
cd setu
npm install
cp .env.example .env.local

Edit .env.local with the URL and publishable/anon key for the Supabase development project. In the Supabase dashboard, open the project (if your organization has more than one, confirm which is the shared development project before continuing — do not point .env.local at a production project), then go to Project Settings → API Keys, "Publishable and secret API keys" tab:

SETU_SUPABASE_URL=https://<project-ref>.supabase.co
SETU_SUPABASE_PUBLISHABLE_KEY=<the "Publishable key" value shown there>

.env.local is ignored by Git. Never put a service-role/secret key or OAuth secret in it — only the publishable key is safe to use here.

2. Configure Supabase Auth

The frontend signs users in with Google OAuth, which needs an OAuth client in Google Cloud plus matching config in Supabase. Do this before your first sign-in attempt — see the note at the end of step 3 if you already tried signing in.

In Google Cloud Console (APIs & Credentials):

  1. Pick or create a GCP project for this app. If prompted, configure the OAuth consent screen first (app name, support email) — this is a one-time step per GCP project. While the app is in "Testing" mode, only accounts you explicitly add as test users can sign in.
  2. Create Credentials → OAuth client ID → Application type Web application.
  3. Under Authorized JavaScript origins, add http://localhost:3000.
  4. Under Authorized redirect URIs, add the callback URL shown in the Supabase panel from the next step, typically https://<project-ref>.supabase.co/auth/v1/callback.
  5. Click Create. Google shows a Client ID (....apps.googleusercontent.com) and Client Secret (GOCSPX-...) — keep this tab open, you'll need both in the next step.

In the Supabase dashboard, under Authentication → Sign In / Providers (this section may just be labeled "Providers" in older dashboard versions), scroll to Auth Providers and click Google:

  1. Paste the Client ID into Client IDs and the Client Secret into Client Secret (for OAuth).
  2. Leave Skip nonce checks and Allow users without an email off — they're workarounds for platforms Setu doesn't need (native iOS flows, providers that omit email).
  3. Toggle Enable Sign in with Google on, then Save.

Without this provider setup, the browser will show Unsupported provider: provider is not enabled.

3. Apply the schema and deploy the API

Authenticate the Supabase CLI and connect it to the development project. supabase login opens a browser tab to authorize the CLI — run it from an interactive terminal, not a non-interactive/CI shell:

npx supabase login
npx supabase link --project-ref <project-ref>
npx supabase db push
npx supabase secrets set SETU_APP_ORIGIN=http://localhost:3000
npx supabase functions deploy api

Comment email delivery

Comments enqueue email rows transactionally in email_outbox. The Apps Script worker in src/EmailDispatcher.ts claims at most 100 pending rows through the Supabase RPC, sends them using the Workspace account's MailApp permission, and marks each row sent or failed. It uses a script lock to prevent overlapping runs and never clears a row before its send succeeds.

After the first Apps Script deployment, run installEmailDispatcherTrigger once from the Apps Script editor. Configure these Script Properties on the Apps Script project: SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY. Set APP_URL to the deployed Setu origin so comment emails can include working “View request” links. EMAIL_SENDER_NAME defaults to Live Stream Setu. Emails use MailApp with noReply: true, matching the existing GCP monitor workflow. The service-role key is stored only in Apps Script properties and must never be placed in frontend code. Set CC_EMAIL to an additional address that should receive a copy of every email; multiple comma-separated addresses are supported. The configured address is added alongside the lead and request participants, with duplicates and the primary recipient removed automatically. Email subjects use the request serial, the same combined request title shown in the detail view, and the start month/year, for example PRG-7 - English Workshop Title | Sep 2026. The body starts with the author's name and comment, renders the full session or inventory list as an HTML <ul>, and ends with a clickable “View request” link to the request in Setu. A plain-text body is included as a fallback.

The GitHub deployment workflow uses the previous clasp flow. It expects the repository secrets CLASPRC_JSON, APPS_SCRIPT_ID, and APPS_SCRIPT_DEPLOYMENT_ID; it runs for version tags (v*) or manually from the Actions tab.

Supabase migrations and the api Edge Function are deployed automatically to both Setu production and Setu Dev by .github/workflows/deploy-supabase.yml when Supabase migrations, functions, configuration, or dependency files change on master. Add the SUPABASE_ACCESS_TOKEN, SUPABASE_PROJECT_REF_PROD, and SUPABASE_PROJECT_REF_DEV as repository secrets, then use the workflow's manual dispatch when needed. The token must have permission for both projects. The workflow selects each project using its secret project ref, applies migrations, and deploys the API function independently to each project; manual dispatch runs both deployment areas.

Email-domain access control

The Admin Settings page manages the allowed email domains. An empty list keeps the app open to all domains; adding the first domain switches the app to allowlist mode. The migration creates the Supabase Before User Created Auth Hook function. For hosted projects, enable it once in Authentication → Hooks using the Postgres function public.restrict_user_by_email_domain (the checked-in supabase/config.toml contains the equivalent local configuration). A refreshed page is enough for an existing session to pick up a changed allowlist.

db push applies the checked-in migrations, including the trigger that creates a profiles row for each new Supabase Auth user. SETU_APP_ORIGIN is required for the API's CORS policy.

Complete this step before your first Google sign-in. If you already signed in earlier (e.g. while testing the OAuth setup in step 2, before db push had run), your auth.users row exists but has no matching profiles row — see Troubleshooting below.

4. Run the frontend

npm run dev

Open http://localhost:3000 and sign in with Google. The dev server uses the same Supabase API transport as the Vercel build.

Useful commands

npm run dev             # Start the local frontend
npm run typecheck       # Type-check backend and frontend
npm run format:check    # Check formatting
npm run build:vercel    # Build the Vercel output in web/
npx supabase db push   # Apply migrations to the linked project
npx supabase functions deploy api

There is no mock backend. If the Edge Function reports that an operation has not been migrated, that operation is not implemented yet.

Troubleshooting

Browser shows "Something went wrong / Failed to send a request to the Edge Function": the api function isn't deployed to your linked project yet. Run npx supabase functions deploy api (step 3).

Browser shows "Something went wrong / Edge Function returned a non-2xx status code": check the function logs in the Supabase dashboard (Edge Functions → api → Logs). An error there reading Cannot coerce the result to a single JSON object means a .single() query got zero rows — almost always because the signed-in user has no matching public.profiles row. This happens if you signed in before running db push, since the profile-creation trigger only fires for auth users created after the trigger exists. Fix it by backfilling the missing row(s) for any existing auth.users from the Supabase dashboard's SQL Editor:

insert into public.profiles (id, email, name)
select u.id, coalesce(u.email, ''),
  coalesce(u.raw_user_meta_data ->> 'full_name', u.raw_user_meta_data ->> 'name', '')
from auth.users u
left join public.profiles p on p.id = u.id
where p.id is null;

This only inserts rows for users missing a profile, so it's safe to re-run. New sign-ins after db push don't need this — the trigger handles them automatically.

If every account has a profile and you still see this error only for an admin/approver account: the profiles RLS policy lets admins/approvers read every profile row, so an unfiltered select('*').single() query for "my own profile" can return more than one row. The Edge Function code filters explicitly by .eq('id', userId) before .single() for this reason — if you're extending supabase/functions/api/index.ts, follow the same pattern rather than relying on RLS alone to narrow a .single() query.

Browser shows "Unsupported provider: provider is not enabled": the Google provider isn't turned on yet, or Client ID/Secret aren't saved — see step 2.

Signing in on a Vercel deployment redirects to localhost instead of the app: that project's Supabase Auth Site URL is still pointing at the local dev default. See Deploy to Vercel step 2.

Deploy to Vercel

Not required for local development — npm run dev (step 4 above) runs the full app against your Supabase dev project without Vercel. This section only applies when you're ready to deploy a preview or production build, and it typically uses its own Supabase project rather than the shared development project from step 1.

1. Connect the repository to Vercel

Set these environment variables for the appropriate Vercel environments (Preview and Production):

  • SETU_SUPABASE_URL
  • SETU_SUPABASE_PUBLISHABLE_KEY

Vercel uses npm run build:vercel and publishes web/ as configured in vercel.json.

2. Point Supabase Auth at the Vercel domain

In the Supabase dashboard, for the project this Vercel deployment uses, go to Authentication → URL Configuration:

  • Set Site URL to the deployed domain, e.g. https://<vercel-domain>.
  • Add that same URL to Redirect URLs (use https://<vercel-domain>/**, or https://*.vercel.app/** to also cover preview deployments).

Skip this and Google sign-in still completes, but the browser is sent back to whatever Site URL happens to be set — often http://localhost:3000, left over from local dev — instead of the Vercel domain.

Also add the Vercel domain to the Google OAuth client from step 2 above: under Authorized JavaScript origins, add https://<vercel-domain>. The Authorized redirect URIs entry doesn't need to change — Google always redirects to the Supabase callback URL, not the app directly.

3. Deploy the database and API to this project

Vercel only builds and serves web/; the database schema and Edge Function are deployed separately. Run these against the same project used in step 1 (pass --project-ref <project-ref> if it isn't the CLI's currently linked project):

npx supabase secrets set SETU_APP_ORIGIN=https://<vercel-domain> --project-ref <project-ref>
npx supabase functions deploy api --project-ref <project-ref>
npx supabase db push --project-ref <project-ref>

Skipping this step is the most common cause of a newly connected Vercel deployment showing "Something went wrong / getDashboard: Failed to send a request to the Edge Function" — see Troubleshooting.

Migration status

The current Edge Function implements whoAmI, getDashboard, and updateOwnProfile. The remaining CRUD and workflow operations are still being migrated; the frontend contract remains in shared/types.d.ts so each operation can be ported incrementally.

The database schema already replaces Sheets-era JSON and comma-separated fields with relational tables. Image storage uses the public request-images bucket for cacheable static assets; uploads and deletes still go through the trusted Edge Function.

Roles

The database roles are admin, approver, viewer, and user:

Role Access
admin Everything, including settings and role management
approver Requests, approvals, scheduling, and read-only users
viewer All requests and standard app sections, including roster and inventory types
user Own and participant requests, plus roster and inventory types

New Supabase Auth users receive a profile through the database trigger. Assign the first development administrator explicitly in the Supabase SQL editor:

update public.profiles
set role = 'admin'
where email = 'your-email@example.com';

About

Internal Web App for managing Studio booking, Equipment Inventory, and staff shift assignment

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages