BeamNG.Earth
BeamNG.drive용 1:1 비율 실시간 지구, 실제 고도·위성 이미지·부동 원점·보조 서버에서 LOD 스트리밍
Friendly bets. Faithful study. An invite-only prediction market for a church friend group: bet play-money Dwell Coin on friendly markets, earn it through Bible study.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Friendly bets. Faithful study.
An invite-only prediction-market app for a church friend group.
www.dwellduel.com · Changelog · How it works · Architecture
Members bet Dwell Coin (DC), a play-money currency, on friendly
questions like "Will the sermon run past noon?" or "Who wins Sunday's
chili cook-off?". They earn more DC by completing Bible-study tasks.
A market maker sells shares at each outcome's current chance, so every
bet's payout is fixed when it's placed; bets can be combined into parlays;
and everything that happens shows up in a live activity feed.README 전문 보기 ↗Members bet Dwell Coin (DC), a play-money currency, on friendly questions like "Will the sermon run past noon?" or "Who wins Sunday's chili cook-off?". They earn more DC by completing Bible-study tasks. A market maker sells shares at each outcome's current chance, so every bet's payout is fixed when it's placed; bets can be combined into parlays; and everything that happens shows up in a live activity feed.
Current release: v1.2.0 · see the changelog.
| Home | A market | Activity | Settings |
|---|---|---|---|
Betting
Coins, tasks and people
App feel
The member-facing rules, with worked payout examples, are in docs/HOW-IT-WORKS.md.
Next.js 16 (App Router, React 19) · TypeScript · Tailwind v4 · Supabase (Postgres, Auth, Realtime, Storage) · Vercel · Vitest · Playwright.
All coin-moving logic lives in Postgres as permission-checked functions; the web app renders, validates and calls them. See docs/ARCHITECTURE.md for the routes, the data model, the key flows and the migrations.
| Doc | For |
|---|---|
| docs/GETTING-STARTED.md | New here? Tools, local Supabase, sign-in, tests and your first PR, in order |
| docs/HOW-IT-WORKS.md | The rules: odds, payouts, parlays, results, tasks, roles, and what data the app keeps |
| docs/ADMIN-GUIDE.md | For reviewers, admins and the owner: invites, resolving, voiding, tasks, members and balances |
| docs/ARCHITECTURE.md | How the code fits together |
| docs/OPERATIONS.md | Running production: deploys, rollback, backups, rotating secrets, incidents, free-tier limits |
| docs/RELEASING.md | Cutting a release |
| AGENTS.md | Conventions every change follows (read this before contributing) |
| CHANGELOG.md | What shipped in each release |
| docs/design/app-redesign-handoff.md | The visual source of truth and the design canvas |
| docs/README.md | An index of every doc, including the archived specs and plans |
Requires Node 24 and Docker (for local Supabase).
npm install # install dependencies
npm run db:start # start local Supabase (Postgres, Auth, Storage) in Docker
npm run db:reset # apply every migration to a fresh local database
npm run dev # start the dev server on http://localhost:3000
Copy .env.local.example to .env.local and fill in
NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY and
SUPABASE_SECRET_KEY from npx supabase status (its API_URL,
PUBLISHABLE_KEY and SECRET_KEY). Required variables are checked when
the server boots (lib/env/required.ts). Signing in locally needs your own
Google OAuth client: see Signing in locally.
npm test # Vitest: unit, component and DB tests (DB tests need local Supabase)
npm run test:e2e # Playwright: builds and serves the app on :3000, so free that port first
npm run lint # ESLint, with no warnings allowed
npm run typecheck # TypeScript
npm run build # production build
npm run check:ios # the installed iPhone app's viewport, in the iOS Simulator (scripts/ios-standalone-check.mjs)
npx vitest run --project unit runs only the tests that don't need the
database. After a migration, regenerate the database types with
npx supabase gen types typescript --local > lib/supabase/database.types.ts;
CI fails if they're stale.
Run npm run db:reset before tests that hit the database. The DB tests
refuse to run against anything but localhost, so they can never touch
production.
node scripts/generate-favicons.mjs rebuilds the favicons, and
node scripts/generate-splash.mjs rebuilds the iOS launch screens. Both
use the symbol's art from components/brand/symbol-paths.ts.
dwell-duel, connected to this repo. Every merge to
main deploys, but through .github/workflows/deploy-production.yml
and a Vercel deploy hook (the VERCEL_DEPLOY_HOOK_URL secret), not
Vercel's Git integration, which vercel.json turns off for main. Functions run in cle1 (vercel.json), next to the
Supabase project in us-east-2. A daily Vercel cron calls /api/cron/keep-alive so the
free-tier Supabase project never pauses (it needs CRON_SECRET).dwell-duel holds the real data. On every merge to
main the same workflow dry-runs supabase db push against production,
and when production is missing any migration it takes an encrypted backup,
pushes the migrations (with the SUPABASE_ACCESS_TOKEN secret) and only
then triggers the app deploy, so new code never runs against an old
schema. What to push comes from production's migration history, so a
migration a failed run left behind goes out with the next merge. It never
runs two at once, and runs only from main. If a backup or migration
fails, nothing deploys; fix it and re-run the workflow from main in the
Actions tab. Its secrets live in the GitHub Production environment,
which only main can deploy to; docs/OPERATIONS.md
lists them..github/workflows/backups.yml
dumps the database nightly and copies the Storage buckets weekly,
encrypted with age, into the private dwell-duel-backups repo.
docs/OPERATIONS.md covers restoring and rolling back.anon / service_role JWT API keys are
disabled. Vercel holds the publishable key
(NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY) and, in Production only, the
secret key (SUPABASE_SECRET_KEY). To rotate the secret key: create a
new one under Supabase → Settings → API Keys, update the Vercel variable,
redeploy, run the keep-alive cron from Vercel → Settings → Cron Jobs to
check it, then delete the old key.https://<project-ref>.supabase.co/auth/v1/callback) as an
authorized redirect URI, and its ID and secret entered under Supabase →
Auth → Providers → Google.https://www.dwellduel.com/auth/google and
https://dwellduel.com/auth/google as redirect URIs on the same client,
list its client ID under Supabase → Auth → Providers → Google → Client
IDs, set NEXT_PUBLIC_GOOGLE_CLIENT_ID in Vercel (Production) and
redeploy. Without the variable, sign-in uses Supabase's redirect. Brand
verification is in docs/OPERATIONS.md.http://localhost:3000/callback, and the
production one) to Supabase → Auth → URL Configuration.CRON_SECRET Vercel has, in Supabase Vault, so pg_cron can call
/api/cron/closing-alerts within a minute of a market closing:
select vault.create_secret('https://www.dwellduel.com', 'app_url'); and
select vault.create_secret('<CRON_SECRET>', 'cron_secret');. The backup
GitHub workflow (.github/workflows/closing-alerts.yml) needs the
CRON_SECRET repository secret and the APP_URL repository variable.VERCEL_TOKEN
secret to the Production environment (Vercel → Account settings → Tokens; it only needs to
read deployments), which lets Deploy Production watch the build to READY;
if the project belongs to a Vercel team, also set the team's id as the
VERCEL_TEAM_ID variable. Keep Vercel's own failed-build email on as well.https://www.dwellduel.com/api/health, and the Sentry–Vercel and
Sentry–GitHub integrations, with set in Vercel.Every pull request runs lint, the type check, a check that the generated
database types match the migrations, the Vitest suite, a production build
and the Playwright suite (.github/workflows/ci.yml) as three parallel
jobs (static: migration order, lint, type check; db: generated types
and DB tests; web: unit tests, build, Playwright) behind one required
check, ci-ok, against a throwaway local
Supabase, never the production database. A PR must be up to date with
main to merge, so main itself isn't tested again: merging deploys.
ci-ok has no bypass, so nothing merges red. CI also fails a PR whose new
migration is numbered at or below main's newest.
Two advisory workflows run beside it, outside ci-ok: a Lighthouse check
on the public pages (.github/workflows/lighthouse.yml), whose budgets only
warn, and a Claude security review of same-repo PRs
(.github/workflows/security-review.yml), which skips until its
CLAUDE_CODE_OAUTH_TOKEN secret is set.
Dependabot opens weekly update PRs for npm packages and GitHub Actions
(.github/dependabot.yml).
Every release, with its notes, is on GitHub Releases, and CHANGELOG.md has the same notes in one file. How a release is cut is in docs/RELEASING.md.
| CONTRIBUTING.md | How changes get in: branches, pull requests and required review |
| SECURITY.md | The security policy: how to report a vulnerability privately, scope and the trust model |
NEXT_PUBLIC_VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY
(the subject is fixed as https://www.dwellduel.com). Generate them once
with npx web-push generate-vapid-keys, and keep them: changing the pair
stops every existing subscription from receiving, until each member
turns notifications on again. Without them production still boots, but
logs "Push notifications are off until they are set" and sends nothing
(lib/env/required.ts only warns about them); locally and in CI,
sending is a no-op without them. The public key is inlined at build
time, so redeploy after setting it.dwellduel.com 308-redirects to www.dwellduel.com, so the
app serves from www. Supabase's Site URL and Redirect URLs must use
www.dwellduel.com. A mismatch makes Google sign-in silently bounce back
to /sign-in.NEXT_PUBLIC_SENTRY_DSN is
set, and a Vercel build holding SENTRY_AUTH_TOKEN uploads its source maps
(org and project default in next.config.ts), then deletes them from the
output. healthchecks.io gets a ping from the daily keep-alive and from each
closing-alerts run (HEALTHCHECKS_KEEP_ALIVE_URL,
HEALTHCHECKS_CLOSING_ALERTS_URL) and emails when one is late or failed;
UptimeRobot watches /api/health, which answers 503 when the app can't
read Supabase. What each alarm means is in
docs/OPERATIONS.md.SENTRY_AUTH_TOKENinsert into public.allowed_emails (email) values ('[email protected]');auth.users row:
update public.profiles set role = 'owner' where id = (select id from auth.users where email = '[email protected]');
From then on, grant Admin and Reviewer from Admin → Members. There is
only ever one owner.