This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
"Aushang" (working title; repo dir is DIGITNEWS) — a digitization layer for old-school
orgs (Kitas, clubs, churches). An admin photographs a paper notice board; the system OCRs
- redacts PII locally, an EU LLM extracts structure, the admin reviews/confirms, and members
get a private feed + calendar + ICS + email digest. It is not a news/RSS reader despite
the repo name. Branding is single-source in
src/config/brand.ts(rename = one-file change).
Production: kita-connect.cloud (domain at Hostinger, app on Vercel, DB/auth on Supabase EU,
email via Resend).
npm run dev # next dev (http://localhost:3000)
npm run typecheck # tsc --noEmit
npm run lint # eslint (type-aware; no-floating-promises etc. are ERRORS)
npm run format:check # prettier --check
npm run build # next build (fails on TS errors; ignoreBuildErrors=false)
npm run verify # typecheck + lint + format:check + build + both secret scans — run before push
npm run gen:icons # regenerate PWA icons from public/icons/master.svg (uses sharp)Secret scans (check:secrets greps the built client bundle; check:source-secrets greps
tracked source) run in verify, in CI, and check:source-secrets also runs in the pre-commit
hook — a server secret reaching the client bundle is a build-blocking failure.
Python worker (worker/, separate toolchain):
cd worker && pip install -e ".[dev]"
ruff check . && ruff format --check . # lint + format
mypy # types
pytest -q # tests (single: pytest tests/test_x.py::test_name)CI (.github/workflows/ci.yml) has two jobs: web (typecheck/lint/format/build/secret-scans)
and worker (ruff/mypy/pytest). It injects placeholder NEXT_PUBLIC_* + service-role env so
the build runs — those are not real secrets.
Note: there is currently no JS test runner wired into main (verify has no test step).
A Vitest suite exists in branch history but was never merged to the remote main.
Authorization is enforced at four independent layers — never collapse them, and call out in any PR if you touch one:
src/proxy.ts— deny-by-default middleware. Everything not on thesrc/lib/routes.tsallowlist requires a validated session (getUser(), nevergetSession()).src/lib/auth.ts—requireSession()/requireAdmin()/requireSuperadmin()resolve the user + DB-backed profile (org + role) at the top of every protected route. Authoritative; trusts no middleware header.- Security-definer RPCs (
supabase/migrations/0002,0005,0007) — the ONLY code that writesprofiles.roleor creates orgs/profiles.search_path-pinned, input-validated, re-check the actor, granted toservice_roleonly. - RLS + column grants (
0003,0006) — DB backstop. Org-scoped; members read only published/confirmed rows. PII columns (ocr_text_raw,ocr_text_redacted,redactions,source_image_path) are column-levelREVOKEd fromauthenticated(0004) — so even an admin's browser/anon client cannot read them. Admin PII access is server-only by construction (service role via a route handler/server component, never the browser client). One deliberate exception (0020, photo consent): a member may see the raw original of a post when they opted in (profiles.photo_consent) AND the admin released it (posts.clear_photo_allowed) — delivered only via a server-minted signed URL (src/lib/photo.ts), never a column read; both flags default false. See SECURITY.md.
superadmin (operator, cross-org) / admin (own org) / member (read-only). Every domain row
carries org_id; helper fns my_org_id(), is_admin(), is_superadmin() drive every policy.
A superadmin's my_org_id() points at their own "Operator" anchor org so org-scoped policies
never widen their reach — cross-org access comes only from dedicated *_superadmin_* policies.
| File | Key | RLS | Use |
|---|---|---|---|
lib/supabase/client.ts |
anon | governed | client components |
lib/supabase/server.ts |
anon + cookies | governed | server components/actions/handlers acting AS the user |
lib/supabase/admin.ts |
service role | bypasses | ONLY the definer RPCs, after our own checks |
lib/supabase/middleware.ts |
anon + cookies | governed | session refresh in proxy.ts |
lib/supabase/admin.ts and lib/env.server.ts both import "server-only" — the compile-time
half of the secret boundary. Never construct a service-role client outside admin.ts.
- LLM advises, deterministic code decides. Nothing publishes without explicit admin
confirmation; all LLM output is schema-validated (
src/lib/content/extraction-schema.ts). Routing reads ONLY the admin-confirmedposts.content_type(nullable, no default —NULLmeans "unconfirmed", deliberately distinct from theinfofallback), never the LLM'scontent_type_suggested(admin-only column, not granted to members). - Privacy by construction. Raw photos never leave our infra; PII is redacted locally (worker: Tesseract → Presidio + spaCy + regex, fail-closed) before any external LLM call. Only redacted text reaches Mistral (EU). Never add a path that sends raw images / unredacted PII anywhere. The Mistral key lives on the worker, never in the web app.
- Deny by default / invite-only. No public signup. Accounts are operator/admin-provisioned.
Keep
enable_signup = false.
/login uses signInWithPassword. There is no standing magic-link login — links
(type=recovery/invite) only ESTABLISH a session that lands on /set-password, where the
user sets a password via updateUser. Invites + forgot-password email a one-time set-password
link via Resend (src/lib/auth-flows.ts → sendPasswordSetupLink), routed through
/auth/callback. /set-password and /passwort-vergessen are in the public allowlist (they
need a link-issued session, not a profile). Login errors are deliberately neutral (no
enumeration). Requires the Supabase dashboard Email provider to have password sign-in enabled.
/aufnahme (admin) compresses the photo client-side → uploads raw to the private raw-photos
bucket via a signed URL → finalizeCapture creates a processing post + triggers the VPS
worker (worker/, FastAPI) with a short-TTL signed URL → worker OCRs/redacts/blurs/extracts →
POSTs /api/worker/callback (shared-secret, constant-time) which writes a draft → admin reviews
on /review and publishes (publish_post RPC: sets confirmed content_type, flips to
published, confirms pending events) → members see it on /feed etc. Until the worker is
deployed, captures upload but stay processing (no worker to run) — the web app works without
it, but the core feature is inert.
content_type routing (src/lib/content/types.ts ROUTING): meal_plan/reflection →
section + post_details; health_notice → top-of-feed alert by severity; event_notice →
events table + ICS; info → general feed.
- Next.js 16 is not the version you may know — read
node_modules/next/dist/docs/before using App Router APIs (perAGENTS.md). React 19, Tailwind v4, TypeScript strict. - Hand-authored DB types in
src/lib/database.types.ts(a Phase-1 stub covering only what's queried; regenerate withsupabase gen typeslater).health_severityetc. aretext+CHECK, not Postgres enums — so a DBORDER BYon them sorts alphabetically, not by severity. Order such columns in code, not SQL. - Input validation is hand-rolled in
src/lib/validation.ts(minimal deps).safeNextPathguards open redirects. - German is the user-facing language; keep UI strings + comments consistent with the codebase.
- Two-org isolation is the headline acceptance test: seed
supabase/fixtures/two_orgs.sqland confirm cross-org reads return zero rows.
docs/ARCHITECTURE.md (system shape + provisioning sequences), docs/GO_LIVE_CHECKLIST.md
(Vercel/Hostinger/Supabase setup), docs/STORE_PRIVACY.md (Apple/Play privacy mapping),
SECURITY.md (adversarial review findings). supabase/config.toml mirrors security-critical
auth settings and is the version-controlled source of truth for them.