An internal operating system for a web-development studio: one place for the whole client lifecycle — lead → client → project → questionnaire → assets → deposit → design → development → approval → final payment → delivery.
Open a client and see everything: who they are, what they bought, what they answered in the questionnaire, which files they uploaded, what’s been paid, what’s left, and what the next action is.
The UI is Hebrew-first (RTL) and fully responsive.
| Layer | Choice |
|---|---|
| Framework | Next.js 16 (App Router, Server Components, Server Actions, Turbopack) |
| Language | TypeScript (strict) |
| UI | React 19, Tailwind CSS v4, Radix primitives, lucide icons, sonner toasts, dnd-kit |
| Data | Supabase — PostgreSQL, Auth, Storage, Row Level Security |
| Validation | zod (server-side on every mutation; shared rules for the public form) |
| Tests | PGlite (real Postgres in-process) for the database, node:test for domain logic |
Fonts: IBM Plex Sans Hebrew (UI), Frank Ruhl Libre (client/project headings), IBM Plex Mono (phones, emails, URLs — always rendered LTR).
npm install
cp .env.example .env.local # then fill in the values (see below)
# apply the database migrations (see "Supabase setup")
npm run create-admin -- you@studio.co.il "a-long-password" "השם שלך"
npm run seed # optional: starter templates + demo data
npm run dev # http://localhost:3000
| Variable | Where | Purpose |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
browser + server | Project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
browser + server | Anon / publishable key (RLS applies) |
SUPABASE_SERVICE_ROLE_KEY |
server only | Public questionnaire endpoints (after token validation) and scripts. Never expose. |
NEXT_PUBLIC_SITE_URL |
server | Base URL for questionnaire links |
npm run db:migrate — set SUPABASE_DB_URL (Connect → Session pooler) in .env.local; applies pending files in order, each in a transaction,npx supabase link --project-ref <ref> then npx supabase db push, orsupabase/migrations/ in order.npm run create-admin -- <email> <password> "<name>"
(or Dashboard → Authentication → Add user). The first user automatically becomes the active owner.
Later users are created pending and must be activated by the owner in הגדרות → צוות.crm-files (25 MB limit, allowed MIME
types enforced by Storage itself). Nothing to click.| File | Contents |
|---|---|
20260925000100_schema.sql |
Enums, tables, constraints, indexes, updated_at/consistency triggers, financial views |
20260925000200_logic.sql |
Activity logging triggers, convert_lead, finalize_questionnaire, ordering + duplication helpers, dashboard_metrics |
20260925000300_security.sql |
RLS on every table, column-level privileges, storage bucket + policies, function privileges |
After changing SQL: npm run test:db (runs every migration on an in-process Postgres and
exercises RLS/functions) and npm run db:types (regenerates src/lib/supabase/database.types.ts).
| Command | What it does |
|---|---|
npm run dev |
Development server |
npm run build / npm start |
Production build / serve |
npm run lint |
ESLint |
npm run typecheck |
Route type generation + tsc |
npm run db:migrate |
Apply pending migrations to SUPABASE_DB_URL |
npm run test:db |
Database test-suite (migrations, RLS, triggers, business functions) — no Docker needed |
npm run test:unit |
Questionnaire condition/validation logic |
npm run db:types |
Regenerate DB types from the migrations |
npm run seed:templates |
Create the starter questionnaire templates (real, reusable) |
npm run seed |
Templates + demo data (leads, clients, projects, payments, tasks, a completed questionnaire) |
npm run seed:reset |
Delete all demo rows (is_demo = true) and everything under them |
npm run create-admin |
Create a staff user via the Admin API |
Demo rows are flagged is_demo and shown with a “נתוני דמו” badge. The app never depends on them.
src/
app/
(app)/… authenticated area (layout = auth gate + shell)
form/[token]/ public questionnaire (no account)
login/, pending/
components/
ui/ design-system primitives (Button, Field, Modal, Confirm, Menu, Badge, tables, …)
shell/ sidebar, top bar, mobile bottom nav + drawer
clients/ projects/ payments/ tasks/ files/ contracts/ notes/ activity/ questionnaires/ form/
lib/
actions/ server actions — every mutation (auth check → zod → DB)
data/ server-only queries
domain/ labels/status model, questionnaire engine (pure, shared client/server)
validation/ zod schemas
supabase/ clients (user / service-role / browser) + generated types
proxy.ts session refresh + optimistic redirect (Next 16 "proxy" = middleware)
supabase/
migrations/ tests/
scripts/ seed, create-admin, type generator
is_staff() (an active row in profiles).
The anon role has no table privileges at all.form_answers rows are inserted only by finalize_questionnaire
(security definer, service-role only). Staff may update internal_note only (column privilege +
trigger guard). Activity logs are append-only./form/<token>), never an ID.
The page and every public server action re-validate the token and the submission state, then
touch only that submission with the service-role client. Internal notes, other clients and
other submissions are never selected.owner / admin / member; only the owner can activate users or change roles
(enforced in the DB). Adding employees later = create user + activate.total_price,
deposit_amount). amount_paid, balance_due and deposit_covered come from the
project_financials view over payments — adding, editing or deleting a payment can’t leave
totals inconsistent. client_financials aggregates per client.client_id from that project by trigger.convert_lead) and reuses an existing client when
the email or phone already exists.finalize_questionnaire): saves answers,
attaches uploads to client/project, creates or de-duplicates the client if needed, marks the
request completed, advances the project status and logs activity.src/lib/domain/forms.ts is shared by the builder, the public form and the server validator:
conditions (equals, not_equals, includes, not_includes, answered, not_answered) are
data on each question, chain correctly (a hidden parent hides its dependants) and are cycle-safe.
Hidden questions are neither validated nor stored. Questions can map to client fields
(maps_to) so a questionnaire sent before a client exists can create the client.
The public form autosaves to the server (debounced) and to localStorage as a backup, warns
before leaving with unsaved changes, and shows per-file upload progress.
date columns (deadlines, payment dates, follow-ups) are calendar dates — formatted in UTC so they
never shift. Timestamps are shown in Asia/Jerusalem. “Month” in revenue metrics = Israeli calendar month.
payments.external_provider/external_id (unique) ready for a gateway webhook.contracts.template_id, signature_provider, signature_request_id for templating/e-signature.activity_logs with typed type +
metadata — a natural trigger source for reminders/emails.DEFAULT_CHECKLIST (per-project, opt-in).