BikeControl (SwiftControl)
Use Zwift Click, Zwift Play, Shimano Di2, or any Bluetooth controller with MyWhoosh, Rouvy, Zwift, and more. Free to try on iOS, Android, Windows, and macOS.
翻訳待ち
No updates detected yet. GitHub releases will appear here.
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗Member platform where Copenhagen Pickleball Collective members book training sessions that match their verified DUPR rating. Member-facing UI is Danish; code and docs are English.
DUPR status: the official DUPR integration is not active yet — it needs DUPR partner credentials. The app ships with a provider adapter, a development/demo-only mock provider, and a documented stub for the live provider. See docs/DUPR_INTEGRATION.md.
| Concern | Choice | Why |
|---|---|---|
| App | Next.js 15 (App Router, server actions), React 19, TypeScript | One deployable unit; server-side rendering keeps rules on the server |
| Database | PostgreSQL + Drizzle ORM (pure JS, SQL migrations) | Row locks for safe booking; no native engine to ship |
| Auth | Better Auth (email/password, DB sessions, password reset, DB-backed rate limiting) | Reputable, self-hosted, no vendor lock-in |
| Validation | Zod on every server input | |
| Images | sharp (re-encodes uploads to WebP) | Strips metadata and payloads |
| Styling | Tailwind CSS v4, self-hosted fonts (Bricolage Grotesque, Figtree) | No third-party font requests (GDPR) |
| Tests | Vitest integration tests on real Postgres; Puppeteer e2e smoke test |
Requires Node 20+ and PostgreSQL 14+.
npm install
cp .env.example .env # then edit: DATABASE_URL, BETTER_AUTH_SECRET
# for local development with fake ratings set: DUPR_PROVIDER="mock" and DUPR_WEBHOOK_SECRET="any-dev-secret"
npm run db:migrate # apply SQL migrations
npm run db:seed # optional demo data (refuses to run in production)
npm run dev # http://localhost:3000
Demo logins after seeding (password demo-pickleball-2026): [email protected], [email protected] (3.412), [email protected] (2.874), [email protected] (3.968).
Without SMTP_URL, emails (sign-up verification, password reset, notifications) land in the dev mailbox at /dev/mail instead of being sent.
To simulate DUPR locally, open /dev/mock-dupr: change a test player's rating or make DUPR fail (server error, rate limit, revoked, no rating), then "Send rating-webhook". This page and its actions return 404 in production unless DEMO_MODE=true.
| Command | Does |
|---|---|
npm run dev / build / start | Next.js |
npm run db:generate | Generate a new SQL migration after editing src/db/schema.ts |
npm run db:migrate | Apply migrations (also creates the settings row) |
npm run db:seed | Demo levels, mock players, users and sessions (is_demo = true) |
npm run admin:grant -- email [--revoke] | Grant/revoke admin. The only way to create the first admin |
npm run tick | Run background jobs once (offer expiry, due DUPR syncs, retention) |
npm run storage:copy | Copy profile photos between storages (see docs/DEPLOY.md) |
npm run lint / typecheck / test / e2e | Quality checks |
See .env.example (placeholders only). Key ones:
DATABASE_URL, BETTER_AUTH_SECRET (≥ 32 chars), APP_URL — required.DUPR_PROVIDER — none (production default until DUPR access exists), mock (dev/demo), dupr (live, once implemented).DEMO_MODE=true — allows the mock provider in a production build and shows a permanent "demo, not real DUPR data" banner. Never set it on the real member site.CRON_SECRET — bearer token for /api/cron/tick.UPLOAD_DIR — persistent disk for profile photos.SMTP_URL, MAIL_FROM — email delivery; without SMTP, emails are logged (dev) or a warning is logged (prod). In-app notifications always work./bekraeftet → /profil to link DUPR. Logging in unverified re-sends the link. Sign-up never reveals whether an email is already registered. Demo-seeded accounts are pre-verified.MEMBER and ADMIN. The role column is not exposed to the auth library, so it can't be set through sign-up or profile updates. Create the first admin with npm run admin:grant -- email; after that admins grant/revoke the role under Admin → Medlemmer → member. Admins can't change their own role, and the club always keeps at least one admin (enforced with row locks). Every change is audited and the member is notified.actor and enforce ownership/role themselves. The middleware redirect is only a convenience.All admin-configurable under Admin → Regler; changes are audited.
Club structure (programmes / "hold") Every session belongs to a programme, and the programme decides who may book:
This is the beginning of the README. Read the rest at the original. Read the full README ↗