Skip to content

Latest commit

 

History

History
150 lines (121 loc) · 10.6 KB

development.md

File metadata and controls

150 lines (121 loc) · 10.6 KB

Local development

Requirements

  • Node 20+ (tested with 25)
  • npm 11+
  • A Supabase account (the free tier is enough)

Initial setup

git clone <repo>
cd HitFactor
npm install
cp .env.example .env.local
# edit .env.local with your Supabase credentials

Variables expected in .env.local:

NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=sb_publishable_...
NEXT_PUBLIC_SITE_URL=http://localhost:3000

NEXT_PUBLIC_SITE_URL

The absolute URL of the site. It is the source of truth for all SEO metadata: canonical, sitemap, robots, Open Graph and JSON-LD. If it is not defined, the local default is http://localhost:3000.

On Vercel it is set per environment (Production / Preview / Development) with each one's real domain — that way the canonical URL and previews point to the right host on every deploy.

Setting up the database

Apply all the migrations in Supabase, in numeric order:

  1. Open the project's SQL Editor.
  2. Paste and run the files in order:

0001 is a squash of the history prior to May 2026, not a full bootstrap. After consolidating it the numbering started over, so 0002+ come after it and are still required: without them you have no is_absent, min_shots, qr_code, is_admin, the ammunition and firearm usage tables, or the Storage bucket for imports. Run all of them, in order.

0025 does not paste and run on its own. It installs a webhook whose shared secret cannot live in this repository — the repository is public — so it reads the secret and your project URL from two session settings and aborts if either is missing. Its header has the two set_config lines to run first, in the same editor tab.

If it aborts, that is the migration working. Do not make it apply by pasting the secret into the file: that puts a live credential in a public repository, where rotating it is the least of the problems. Everything else here really is paste-and-run.

0026 needs pg_cron enabled first (Supabase → *Database → Extensions → pg_cron). It also aborts rather than applying halfway, and for the same reason: it installs a log of feedback-notification outcomes — deliveries as well as failures — and a log that nothing ever writes to is indistinguishable from one with nothing to report. It also needs 0025's trigger already in place — it checks for it by name. Log rows are kept for 90 days.

# File What it does
0001 0001_initial_schema.sql Consolidated base schema: profiles, disciplines, divisions, shooters, matches, stages, match_entries, stage_results, firearms, match_firearm_log, clubs, audit_log, feedback + indexes, triggers, RLS and seeds
0002 0002_my_discipline_counts.sql my_discipline_counts(p_user_id) RPC — aggregates the user's participation count per discipline in Postgres (consumed by the sidebar)
0003 0003_security_advisor_fixes.sql Supabase Security Advisor fixes: fixed search_path on functions and the remaining linter warnings
0004 0004_fat_pdf_source.sql Extends the matches.source_type CHECK with fat_pdf (official FAT rankings in PDF)
0005 0005_profiles_admin.sql profiles.is_admin — enables the diagnostic views restricted to administrators
0006 0006_match_dedup_utilities.sql Audit and cleanup utilities for entries duplicated by re-import (merge_duplicate_shooters and friends)
0007 0007_match_entries_absent.sql match_entries.is_absent — distinguishes "did not compete" from "competed and did poorly"
0008 0008_match_entries_update_extended.sql Widens the UPDATE RLS on match_entries: besides the importer, admins and the shooter themselves
0009 0009_backfill_is_absent.sql Backfills is_absent on matches imported before 0007 (avoids re-importing)
0010 0010_ammunition_types.sql Per-shooter ammunition catalog, factory and reload (#46)
0011 0011_firearm_usage_log.sql Firearm usage log outside matches — training and practice (#47)
0012 0012_firearm_qr_codes.sql Short code per firearm + /q/{code} route, so the QR stuck on the firearm stays small (#58)
0013 0013_firearm_qr_code_default.sql Replaces the trigger that generated qr_code with a column DEFAULT, so gen types sees it as optional
0014 0014_match_min_shots.sql matches.min_shots — the basis of the "extra rounds" KPI against rounds_fired (#75)
0015 0015_steel_pdf_source.sql Extends the source_type CHECK with practiscore_steel_pdf (Steel Challenge PDFs)
0016 0016_fbi_classic_division.sql CLASSIC division for Tiro FBI
0017 0017_ipsc_classic_manual_division.sql CM (Classic Manual) division for IPSC — shotgun exported from PractiScore
0018 0018_update_ui_prefs_rpc.sql update_ui_prefs RPC — atomic merge of ui_prefs, avoids lost updates (#126)
0019 0019_fbi_optic_division.sql OPTIC division for Tiro FBI
0020 0020_import_uploads_storage.sql Private match-imports bucket + per-user RLS policies, for staging import files
0021 0021_fix_shooters_claim_rls.sql Fixes the UPDATE RLS on shooters: its using clause let any authenticated user edit or unlink someone else's shooter (#195)
0022 0022_matches_delete_admin.sql matches_delete_admin — completes the admin scope over matches that 0014 started, so admin authority matches what the app offers (#197)
0023 0023_rls_to_clause_and_initplan.sql Rewrites every pre-0021 policy to to authenticated + (select auth.uid()) — drops the deprecated auth.role() and clears the auth_rls_initplan advisory. No predicate changes (#207)
0024 0024_steel_rfri_division.sql RFRI (rimfire rifle, labelled Minirifle) for Steel Challenge — the division was missing, so any match running it failed to import
0025 0025_feedback_telegram_webhook.sql feedback_to_telegram — the trigger that posts new feedback to the feedback-telegram Edge Function. Existed in production since June, created by hand and never committed. Needs two session settings to apply; see the file's header
0026 0026_feedback_notification_trail.sql ops.feedback_notification_log + a pg_cron sweep that copies each feedback webhook's outcome out of net._http_response before pg_net purges it, so a lost notification leaves a trace that outlives the hour nobody was watching (#288). Kept for 90 days, with a heartbeat so a stopped sweep says so rather than reading as healthy. Needs pg_cron and 0025's trigger; aborts without either
0027 0027_correlate_sweep_join_by_time.sql Correlates the sweep's net._http_response lookup by time as well as by id. pg_net's request ids restart if the extension is recreated, and supabase_functions.hooks is never purged — so an id alone could hand an old hook a newer delivery's outcome, and report a failure as delivered. Paste-and-run; needs 0026

If you add a migration, add its row here: there is a test (tests/migrations-doc.test.ts) that fails if the directory and this table drift apart. This table was 18 migrations out of date precisely because nothing verified it.

For development we recommend disabling email confirmation in Supabase → Authentication → Sign In / Providers → Email — that way you can create test accounts without having to confirm them.

Commands

npm run dev          # dev server (localhost:3000)
npm run build        # production build
npm run lint         # eslint (errors fail, warnings are printed)
npm test             # run the tests once
npm run test:watch   # tests in watch mode
npm run db:types     # regenerate src/lib/supabase/database.types.ts from the DB

db:types needs a SUPABASE_ACCESS_TOKEN in the environment (or a prior supabase login). Run it after applying a migration that changes the schema, so the typed Supabase client stays in sync.

You will be told if you forget. That file is what supabase-js derives every row type from, so when it drifts tsc keeps checking against a schema that no longer exists and says nothing (#269).

tests/database-types-drift.test.ts applies every migration to a real Postgres compiled to wasm and compares the schema against the committed file: base tables, their columns, nullability, type class, and the functions PostgREST exposes. It needs no token and no network, so it runs in CI like any other test. It does not look at Insert/Update shapes, Enums, Views, CompositeTypes or relationships — the test header says why, and where to extend it.

Project structure

See architecture.md.

Quick check after making changes

npm run lint && npx tsc --noEmit && npm test && npm run build

All four have to pass. Currently: 847 tests green, typecheck clean, lint clean (with the three warnings explained in eslint.config.mjs).

Note that npx tsc --noEmit and npm run build are not interchangeable. next build also type-checks against the route types Next generates from the directory structure (PageProps<...>, LayoutProps<...>), which plain tsc never sees. A params type that satisfies tsc can still fail the build.