Bilingual (CZ / EN) event-registration platform for Diamond Way Buddhism (BDC) centres.
Public visitors register themselves and fellow participants for meditation and community events; the app prices the stay server-side and emails a bilingual confirmation. Centre admins manage events, registrations and exports — all scoped by role and centre.
- What it is
- Screenshots
- Feature highlights
- Tech stack
- Architecture
- Security & privacy
- Internationalization
- Getting started
- Environment variables
- npm scripts
- Project structure
- Testing
- Deployment
- Documentation
- Status & roadmap
- License & credits
Registrace is a production web application built for Buddhismus Diamantové cesty (BDC / Diamond Way Buddhism) — a network of Czech meditation centres. It replaces ad-hoc spreadsheets and email threads with a single bilingual flow:
- A visitor opens a published event, fills in one form for the whole group (up to 10 participants), picks arrival/departure, meals and diet per person, and submits.
- The server recalculates every price from the event's own pricing rules (the browser
figure is informational only), writes an idempotent registration, and sends a confirmation
email carrying a human-readable registration number (e.g.
260020108). - Centre admins review, filter, search, mark paid, resend confirmations, and export a per-event XLSX for the kitchen and accommodation teams — always scoped to the centres they manage.
It is live in production at registrace.online on Vercel + Supabase, and has been through a full internal build (B1–B8), a production-hardening pass (P1–P8) and a multi-agent security audit. This README is the single source of orientation for anyone joining the project.
All screenshots show a demo dataset — fictional families on the RFC-2606 reserved
example.*domains, never real registrants. The admin email addresses and the audit-log IP column are blurred here on purpose for privacy: they are shown normally to admins in the running app — only these public screenshots hide them.
![]() |
![]() |
| Homepage — published events (CZ) | Same page, one click to English |
![]() |
![]() |
| Event detail + stay (arrival, departure, accommodation) | Per-participant age, price tier, diet & live total |

Transparent, data-driven price overview — meals, daily rates per age and tier, arrival/early-departure discounts.

Per-event kitchen & accommodation planning — meat/veg counts per meal and headcount per night, ready for the kitchen and accommodation teams.
- Bilingual throughout — every page and email in Czech or English; the switch preserves the current place.
- Group registration — one submission for up to 10 participants, each with their own age category, price tier (standard / supported / surplus) and diet (meat / vegetarian).
- Per-day meal selection with a per-event meal-ordering deadline (after the cut-off, meal choice is closed and enforced server-side).
- Live, server-authoritative pricing — the form shows a running total, but the backend always recomputes the authoritative price before saving.
- Arrival time, early departure and accommodation all feed into the price via the event's own discount and night-rate rules.
- Human-readable registration number (
YYEEENNNN) and a polished, BDC-branded confirmation email. - Privacy-first — in-app GDPR consent, server-validated honeypot, idempotent submit. No cookie banner, because nothing here needs consent: the only cookies are the strictly necessary ones (the admin session and the chosen locale), there is no ad tech or cross-site tracking, and the only analytics is Vercel's cookieless page counter (details).
- Role-based access —
SUPER_ADMINsees everything;ADMINis scoped to their assigned centre(s); an owner tier guards super-admin management. - 7-step event wizard — bilingual titles/descriptions, dates, pricing rules per age & tier, meals per day, capacity, and the meal deadline. Empty drafts stay fully editable.
- Event lifecycle — draft → published → closed → archived, with a public visibility window derived on read.
- Registration workflow — filter by centre / status / archived, search by number, edit status (registered / paid / cancelled), resend confirmations, and read kitchen (meat / veg totals) and accommodation (per-night headcount) tables.
- Per-event XLSX export — one click per event, with a formula-injection-safe serializer.
- Centre & admin management — invite/edit/remove admins, assign centres, soft-delete and restore centres. An invited admin lands on a guided password setup: the requirements are listed up front and tick as they are met, with a show/hide toggle and a live match check (details and the important caveat under Security & privacy).
- Audit log — a forensic trail of admin actions (actor, action, entity, IP, time).
| Layer | Choice | Notes |
|---|---|---|
| Framework | Next.js 16 (App Router, Turbopack) · React 19 | Server Components + route handlers; no middleware.ts — edge logic lives in proxy.ts |
| Language | TypeScript (strict, noUncheckedIndexedAccess) |
— |
| ORM | Prisma 7 + @prisma/adapter-pg (pg) |
Driver-adapter pattern; client generated to generated/prisma (gitignored) |
| Database & Auth | Supabase (PostgreSQL + Auth) | RLS deny-all; all data access goes through Prisma |
| Validation | Zod 4 | Client-safe schemas (no Prisma imports) |
| Forms | React Hook Form + @hookform/resolvers |
— |
| i18n | next-intl 4 | Locales cs (default) / en |
| Resend | Bilingual, inline-CSS, non-blocking | |
| Export | exceljs | XLSX (chosen over the vulnerable xlsx package) |
| Styling | Tailwind CSS v4 | Design tokens via @theme in globals.css, no JS config |
| Tests | Vitest (+ v8 coverage) | 134 unit / integration tests |
| Analytics | Vercel Web Analytics | Cookieless page analytics; the only third party in the page |
| Hosting | Vercel + own domain (Wedos DNS) | Auto-deploy on push to main |
Exact versions live in package.json. No Docker.
These invariants are enforced across the codebase (full list in
CLAUDE.md):
- Auth = Supabase Auth. Data = Prisma. Never mix.
- The pricing engine (
modules/pricing) is pure, server-only, no DB access. - Frontend prices are informational; backend prices are authoritative and always recomputed server-side before any DB write.
- UI text lives in next-intl JSON; event content lives in bilingual DB columns
(
*_cs/*_en). - Email failure never rolls back the registration transaction.
- Soft delete (
deletedAt) everywhere — no permanent deletion of audit-relevant data. - Money = whole-CZK integers; datetimes = UTC in the DB, Europe/Prague in the UI.
- Registration submit is idempotent (client-supplied UUID v4 key), honeypot-guarded, and capped at 10 participants.
- SUPER_ADMIN sees all; ADMIN is scoped to their centre(s).
flowchart LR
subgraph Client["Browser (CZ / EN)"]
Pub["Public pages<br/>+ registration form"]
Adm["Admin panel"]
end
Edge["proxy.ts — edge middleware<br/>matcher: pages + /api/admin/** only<br/>i18n routing · CSP nonce · session refresh<br/>admin API: rate-limit · CSRF · 401 gate"]
subgraph Next["Next.js server"]
Pages["Server Components<br/>app/[locale]/**"]
PubAPI["Public handlers<br/>/api/events · /api/registration/**<br/>rate-limit in handler"]
AdmAPI["Admin handlers<br/>/api/admin/**<br/>role + ownership guard"]
SVC["Services<br/>modules/**"]
Price["Pricing engine<br/>modules/pricing (pure)"]
end
DB[("Supabase<br/>PostgreSQL")]
AuthSvc["Supabase Auth"]
Mail["Resend<br/>(email)"]
Pub -->|"page requests"| Edge
Adm -->|"page requests"| Edge
Edge --> Pages
Adm -->|"manage"| Edge
Edge --> AdmAPI
Pub -->|"submit / calculate-price<br/>(edge NOT in path)"| PubAPI
Pages --> SVC
PubAPI --> SVC
AdmAPI --> SVC
SVC --> Price
SVC -->|Prisma| DB
Adm -.->|"signInWithPassword"| AuthSvc
Edge -.->|"session"| AuthSvc
SVC -.->|"non-blocking"| Mail
- Edge (
proxy.ts) is deliberately not a global gate: itsconfig.matchercovers pages and/api/admin/**only. On pages it does locale routing, the CSP nonce and Supabase session refresh; on the admin API it adds rate-limiting (120/min/IP), a CSRF same-origin check on mutations, and a 401 for anonymous callers. It checks session presence only. - The public API bypasses the edge entirely —
/api/events,/api/registration/**and/api/auth/meare excluded by the matcher and reach their handlers directly, so each one enforces its own rate limit (submit 10/h, price 60/min, public reads 60/min per IP). - Handlers/services are the authoritative role/ownership gate (Prisma can't run at the
edge). Business logic never lives in a route handler — it lives in
modules/*, which Server Components call directly rather than fetching their own API. - Prices are computed by the pure engine and re-verified before every write.
modules/pricing/index.ts is pure and defensive (a missing rule or degenerate stay yields
0, never throws — the price endpoint calls it mid-edit on incomplete input). Per participant:
participation = dailyRate × days
− arrival discount (by arrival time: morning / afternoon / evening)
− early-departure discount
+ nightRate × (days − 1) (only when accommodation is chosen)
floored at 0
meals = Σ price of each unique, open, known selected meal
subtotal = participation + meals
Pricing is data-driven: every age is charged by its matching PricingRule.dailyRate — no
age is hard-coded to 0. Young children carry a 0-rate rule, but an event may charge, say,
ages 8–14 (the real BDC "MLK" course does, at 100 CZK/day). Discounts apply to 15+ only
because child rules carry 0 discounts — not via any age branch.
11 Prisma models, 9 enums, 6 applied migrations. The source of truth is
prisma/schema.prisma.
Models (click to expand)
| Model | Purpose |
|---|---|
| User | id @db.Uuid (= Supabase Auth id), email, role. |
| UserCenter | Explicit User ↔ Center join (which centres an admin manages). |
| Center | 25 seeded rows — 23 BDC centres plus the Jiné / Mimo ČR (Other / outside CZ) catch-alls a visitor can pick as their home centre. Bilingual names, sortOrder, soft-active. |
| Event | Bilingual title/subtitle/description, contact fields, status, centerId (host centre), dates, createdBy, mealRegistrationDeadline, numberPrefix + registrationSeq (reg-number support). |
| EventDate | A day of the event; used as arrival/departure reference. |
| PricingRule | Per event × ageCategory × pricingType: dailyRate, nightRate and the four *Discount fields (subtracted). |
| EventMeal | A meal slot on a given day: mealType, price, isClosed. |
| Registration | The submission: home centerId, arrival/departure, hasAccommodation, email, gdprConsent, totalPrice, status, idempotencyKey, registrationNumber, locale, ipAddress. |
| Participant | One person: ageCategory, pricingType, mealType (diet), computed prices. |
| ParticipantMeal | Participant ↔ EventMeal join with the charged price. |
| AuditLog | Append-only trail: userId, action, entityType, entityId, oldData/newData, ip. |
Enums: AgeCategory · PricingType (standard / supported / surplus) · ArrivalTime ·
EarlyDeparture · EventStatus (draft / published / closed / archived) · MealType
(breakfast / lunch / dinner) · MealCategory (meat / vegetarian) · RegistrationStatus
(registered / cancelled / paid) · UserRole (super_admin / admin).
Routes (click to expand)
Public (not matched by proxy.ts — each handler rate-limits itself)
GET /api/events·GET /api/events/[id]— 60/min per IPPOST /api/registration/calculate-price— 60/min per IPPOST /api/registration/submit— 10/hour per IP
Admin (edge: session + rate-limit + CSRF; handler: role/ownership)
- Events —
GET/POST /api/admin/events,GET/PUT /api/admin/events/[id],PATCH /api/admin/events/[id]/status - Registrations —
GET /api/admin/registrations,GET/PUT /api/admin/registrations/[id],POST /api/admin/registrations/export,POST /api/admin/registrations/[id]/resend-confirmation - Centres —
GET/POST /api/admin/centers,PUT/DELETE/PATCH /api/admin/centers/[id](DELETEsoft-deletes,PATCHrestores) - Admins —
GET/POST /api/admin/users,PUT/DELETE /api/admin/users/[id],POST /api/admin/users/[id]/reset-password - Audit —
GET /api/admin/audit-log
Auth — GET /api/auth/me (60/min per IP; login/logout go through the Supabase browser
client, not a route handler).
Validation errors return a canonical 400 { error, details } (Zod issues) via the shared
validationError() helper.
- Content-Security-Policy with a per-request nonce +
strict-dynamic(set inproxy.ts), droppingunsafe-inlinefromscript-srcin production. - Static security headers in
next.config.ts, applied to every response —Strict-Transport-Security: max-age=31536000; includeSubDomains; preload,X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy: strict-origin-when-cross-origin,Permissions-Policy: camera=(), microphone=(), geolocation=()andCross-Origin-Resource-Policy: same-origin. (Thepreloaddirective is a one-way commitment and only takes effect once the domain is submitted at hstspreload.org.) - CSRF — mutating admin requests must be same-origin, checked against
NEXT_PUBLIC_APP_URL. Fail-closed: a missing Origin and Referer, or an unsetNEXT_PUBLIC_APP_URL, is rejected; the any-localhost relaxation is gated to non-production. - Rate limiting — best-effort in-memory limits: admin API 120/min/IP at the edge; submit 10/hour, price calc 60/min and public reads 60/min enforced inside each public handler.
- Audit log — best-effort, non-blocking; never rolls back the business write.
- No browser-direct data access — the Supabase anon key is used for Auth only; the JS client never reads or writes tables. Every data access goes through Prisma on the server, which connects directly and bypasses RLS by design.
- RLS — enabled deny-all on all 13 public tables (the 12 models +
_prisma_migrations): row security is on and zero policies are defined, so nothing is reachable through the anon key. This is a backstop, not the access control — the real authorization is the role/ownership gate in the handlers and services. It lives in the migrations (20260721104500_enable_rls_on_all_tables) andprisma/rls.test.tsfails the build if a model is added without it. That is a correction, not a preference: RLS was originally set by hand in the Supabase dashboard, which covers the tables that exist at that moment — soMealPricingRule, created by a migration, went live with RLS off andanongranted SELECT/INSERT/UPDATE/DELETE on it through PostgREST. Verify the live state withselect tablename, rowsecurity from pg_tables where schemaname = 'public'(expect alltrue) andselect * from pg_policies where schemaname = 'public'(expect no rows). Supabase's Security Advisor reports this as informational "RLS Enabled No Policy", which is the intended state here. - Owner tier — only an owner (
OWNER_USER_IDS, immutable Supabase ids; verified-emailOWNER_EMAILSfallback) may create/modify super-admins. Both lists empty → nobody can (fail-closed). - Admin password policy — at least 12 characters with a lowercase letter, an uppercase
letter, a digit and a symbol, shown as a live checklist while the password is typed, with a
per-field show/hide toggle and a live match indicator on the confirm field
(
lib/validation/password). Note the split, which mirrors the pricing rule: admin passwords are set by the browser calling Supabase Auth directly, with no route of ours in between, so the checklist is informational and the authoritative gate is the policy configured in the Supabase project (Authentication → Providers → Email), currently minimum length 12 + lowercase, uppercase, digits and symbols. The two must be kept in sync, and the client must never be the laxer of the pair — a checklist that goes all-ticks on a password Supabase then refuses is worse than no checklist.
The subtle part: GoTrue validates withstrings.ContainsAnyagainst literal ASCII sets, not Unicode categories.Žis not an uppercase letter to it and§is not a symbol, so the rules here mirror those exact sets (and the labels say "a–z" / "A–Z" out loud, because otherwise a Czech admin typesŽ, reads "uppercase ○" and assumes the form is broken). Length is the one deliberate asymmetry: we count characters where GoTrue counts bytes, which makes us stricter on accented input — the safe direction. - GDPR — explicit
z.literal(true)consent; the storedipAddressis retained solely for abuse prevention and never appears in the UI or exports. - Analytics — Vercel Web Analytics
(
<Analytics />inapp/layout.tsx) runs on every page. It is cookieless and does not fingerprint or track visitors across sites; its beacon posts to/_vercel/insightson this origin, which is whyconnect-src 'self'covers it. It is the only third party in the page, and it never sees registration data — that all moves over our own API. - Export hardening — XLSX cells are neutralized against spreadsheet formula injection
(
=,+,-,@, tab and CR are prefixed) across title, headers and every data row. - Idempotency & honeypot on the public submit path; max 10 participants. The honeypot is re-checked in the service layer, and an idempotency-key race is recovered on the unique constraint rather than surfacing an error.
- Routing and UI copy use next-intl 4; locales are
cs(default) anden, prefixed in the URL (/cs/...,/en/...) and handled inproxy.ts. - UI strings live in
locales/cs.json/locales/en.json; keys are namespaced (form,home,event,badge,admin, …). - Event content is bilingual in the database (
title_cs/title_en, etc.) — not in the locale files — so admins author both languages per event. - The confirmation email renders in the visitor's original locale (persisted on the registration), so a later admin resend stays in the right language.
- Node.js 20+ (the tooling uses the built-in
fetch/WebSocket) - A Supabase project (PostgreSQL + Auth)
- A Resend account + API key (for confirmation emails)
git clone https://github.com/Martin8O/Registrace.git registrace
cd registrace
npm install # runs `prisma generate` via postinstallcp .env.example .env.local
# then fill in the values — see “Environment variables” below# Apply all migrations to your database (uses DIRECT_URL)
npx prisma migrate deploy
# Seed the centre rows (23 BDC centres + 2 catch-alls) — and nothing else.
# Idempotent, deletes nothing, safe to re-run.
npx prisma db seedCreate events and registrations from the admin panel. There is deliberately no demo data
anywhere in the setup path: the seeder creates only the centre rows every instance needs.
Two demo-data paths existed during the build and both are gone — one truncated Event and
Registration before reseeding, the other put a fictional published event on the public
homepage of whatever instance ran the documented setup command. Both are in the git history
if a throwaway environment ever wants them.
npm run dev # http://localhost:3000Admins sign in with Supabase Auth at /<locale>/admin/login. The first SUPER_ADMIN is
provisioned manually: create the user in Supabase Auth, then set their role in the database
(e.g. npx tsx --env-file .env.local prisma/promote-super-admin.ts <email> once their User
row exists). From then on, further admins are invited from the panel's Admins (Správci)
screen, which assigns roles and centres.
Note: opening an invite/reset link signs that browser in as the link's user, replacing any session already present in every window (cookie auth is one session per browser). This is deliberate — the identity must come from the token, never from whoever happens to be logged in. If you're testing an invite while signed in as a super-admin, open it in a private window to keep your own session; the set-password page also states whose account it is.
Copy .env.example to .env.local. All are required in production unless
noted.
| Variable | Purpose |
|---|---|
DATABASE_URL |
Pooled Supabase connection (port 6543) — used by the app at runtime. |
DIRECT_URL |
Direct Supabase connection (port 5432) — used by Prisma migrate/seed. |
NEXT_PUBLIC_SUPABASE_URL |
Supabase project URL (also feeds the CSP connect-src; must be set at build). |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Supabase anon key (browser auth client). |
SUPABASE_SERVICE_ROLE_KEY |
Service-role key for admin user management (server only). |
RESEND_API_KEY |
Resend API key for confirmation emails. |
NEXT_PUBLIC_APP_URL |
The app's own origin — used for invite/reset links and the admin CSRF check. A wrong value silently 403s every admin write. |
EMAIL_FROM |
Verified sender, e.g. BDC Registrace <noreply@send.registrace.online>. |
OWNER_USER_IDS |
Comma-separated Supabase Auth user UUIDs allowed to manage super-admins (preferred, immutable). Find them in Supabase → Authentication → Users. |
OWNER_EMAILS |
Legacy fallback — verified emails allowed to manage super-admins. Both owner lists empty → nobody can manage super-admins. |
SUPER_ADMIN_EMAIL |
Optional, tooling only. Fallback address for prisma/promote-super-admin.ts when no argument is passed. Not read by the app. |
NEXT_PUBLIC_*values are inlined at build time; on Vercel they must be present when the build runs.EMAIL_FROMis a runtime value, so changing it needs a redeploy.
| Script | What it does |
|---|---|
npm run dev |
Start the dev server (Turbopack) on :3000. |
npm run build |
Production build. |
npm start |
Serve the production build. |
npm run lint |
ESLint. |
npm test |
Run the Vitest suite once (CI-friendly). |
npm run test:watch |
Vitest in watch mode. |
npm run test:coverage |
Vitest with v8 coverage. |
postinstall |
prisma generate (regenerates the gitignored client). |
Database utilities: npx prisma migrate deploy (apply migrations), npx prisma db seed
(centre rows), prisma/promote-super-admin.ts (bootstrap a super-admin).
app/
[locale]/(public)/ public pages (home, event detail + registration form)
[locale]/admin/(panel)/ admin panel — dashboard, events, registrations,
centres (/admin/centers), admins (/admin/users), logs, profile
[locale]/admin/login|set-password|auth/confirm auth entry points
api/ route handlers (public + admin) + _lib (guard, http helpers)
components/{public,admin,shared} UI components
modules/{events,registrations,pricing,auth,centers,users} business services (no fat handlers)
lib/ infrastructure
{db,security,email,export,supabase,utils,mock,admin}/ modules
validation/ client-safe Zod schemas + the admin password policy
audit.ts · types.ts audit-log writer · shared types
auth-errors.ts Supabase auth-error code → next-intl key
locales/{cs,en}.json UI translations
prisma/
schema.prisma · migrations/ data layer (6 applied migrations)
seed.ts the 25 centre rows (no demo data — see Getting started)
promote-super-admin.ts one-off super-admin bootstrap
public/images/ static assets (BDC logo)
proxy.ts edge middleware (i18n + session + admin hardening + CSP)
i18n/request.ts next-intl request config
next.config.ts static security headers
prisma.config.ts Prisma CLI config (reads DIRECT_URL)
vitest.config.ts test runner config
generated/prisma/ generated Prisma client (gitignored)
docs/screenshots/ README images
Note the naming: the “centres” screen lives at /admin/centers and the “admins” screen at
/admin/users (the route names use the model names).
npm test runs 134 Vitest tests across 10 files, with no database required:
- Pricing engine (43) — the arithmetic against the hand-derived BDC formula, grouped by
concern: children on a
0rule, ages 8–14 on a configured rate, 15+ per tier, discounts subtracted, accommodation nights, meal pricing per age × tier (a child's lunch priced differently from an adult's, and the tier moving both), the flat-price fallback that keeps pre-matrix events billing exactly what they always did, defensive behaviour (missing rule, degenerate stay, over-large discount →0, never a throw) and the full aggregated result. - Validation (13) — the Zod submit/price schemas (honeypot, participant caps, the tier accepted at every age but still bounded by its enum, diet).
- Submit service (9) — control-flow with a mocked Prisma (
vi.mock('@/lib/db')) while keeping the real engine, sototalPriceis asserted end-to-end. - CSRF origin gate (13) — that the admin origin check accepts the canonical origin and a
Vercel preview's own url, and rejects everything else: foreign origins, a missing
Origin + Referer, localhost in production, and — the regression that matters — a
vercel.apporigin while running in production. - Export & auth (7 + 4) — the registration-export scoping (including the cross-centre IDOR regression) and the owner-tier auth helpers.
- Auth error wording (8) — the Supabase-code → message mapping, plus a check that every key it can return is translated in both locales (an unmapped key would render as raw text).
- Password policy (24) — that the rules mirror GoTrue's literal ASCII sets (Czech accented letters and non-ASCII symbols must not tick a rule, or the checklist would green-light a password Supabase rejects), that the checklist and the submit gate can never disagree, and that every rule is labelled in both locales.
- RLS guard (3) — that every model's table has
ENABLE ROW LEVEL SECURITYin a migration, and that no migration defines a policy or forces RLS on the owner. It reads the schema and the migration SQL, not the database. It exists because RLS used to be a dashboard setting: it covered the tables that existed when it was clicked, andMealPricingRulearrived later through a migration with RLS off andanonholding read/write on it. - Docs guard (10) — the counts on this page. Every number above is parsed back out of the
README and checked against the test files (via the TypeScript AST, so
it.eachexpands and regex literals aren't mistaken for code), as are the badge, the tech-stack row andAGENTS.md. It also checks that every counted file is one Vitest is configured to run, so a test outside theincludeglobs can't inflate the total with cases nobody executes. It exists because "a 22-scenario matrix" outlived the matrix by two audits: both checked the total, which was right, and trusted the prose beside it.
- Hosting: Vercel (serverless), auto-deploying every push to
main(~1–2 min). - Database/Auth: Supabase (
eu-west-1). Migrations are applied withprisma migrate deploy(a no-op when already in sync). - Domain:
registrace.online— apex canonical,www→ 308 → apex, DNS kept at Wedos. - Email: Resend sends from the verified subdomain
send.registrace.online(DKIM/SPF/DMARC), isolating sending reputation. - Build note: the Prisma client is gitignored and regenerated on Vercel via the
postinstallhook; allNEXT_PUBLIC_*vars must be set at build time.
| Document | What's in it |
|---|---|
CLAUDE.md |
Project constitution — the 20 architectural invariants, roles, folder map and translation-key conventions the code is held to. Written for Claude Code, which built the app; readable as plain architecture notes. It also references a local/ workspace that is gitignored and not published — see the note at the top of the file. |
AGENTS.md |
Briefing for AI coding agents (the agents.md convention, read by Claude Code, Cursor, Copilot and others) — commands, the non-negotiable rules, and the things about this codebase that surprise people. Its top block is Next.js-managed and points agents at the version-matched docs bundled in node_modules/next/dist/docs/. |
.env.example |
Annotated environment-variable template — every variable with its purpose, where to find its value, and the build-time vs runtime distinction. |
LICENSE |
MIT. |
This README is the only document a reader needs; the rest are supporting detail.
The full build (B1–B8) and production-hardening (P1–P8) phases are complete, and the app is deployed and verified in production. A multi-agent security audit has been run and its findings fixed.
Known parking-lot items (non-blocking):
- Persist a form draft so switching language mid-registration doesn't reset the form.
- Move the in-memory rate-limiter to a shared store (Upstash/Postgres) if serverless scale demands it.
- Optional granular Supabase RLS policies, should any browser-direct data reads ever be added (none are currently planned).
Licensed under the MIT License — see LICENSE. The code is open to read, learn
from and reuse.
The Buddhismus Diamantové cesty (BDC) name, logo and visual identity belong to BDC and are not covered by the MIT grant, which applies to the source code only.
Built with Next.js, Prisma, Supabase, next-intl, Zod, React Hook Form, Resend, exceljs, Tailwind CSS and Vitest.
Designed and built by Martin Svoboda — svobodamartin.dev.










