# ConnectIQ — workspace knowledge

Binding on every ConnectIQ project in this workspace. Generated from the architecture appendix:
a rule changes there first and this file is regenerated from it. **Never edit a rule here to make
a feature easier.**

Project knowledge holds the phase-specific detail — stack, environments, visual reference values.
It adds specifics; it does not override a rule in this file. **If a request conflicts with a rule
here, follow the rule and say so. Do not silently resolve the conflict.**

## Shape of the system

One Postgres database, one deployable, separated by **domain prefix** — not microservices, not
separate schemas. Isolation is the prefix plus the folder `src/domains/<domain>/`, and the folder
structure exists before you build into it.

Eleven prefixes, locked: `cl_` Customer & Location · `com_` CRM/Commerce · `plan_` Sales Handover ·
`ops_` Field Operations · `inv_` Inventory/Asset · `sub_` Subscription · `sup_` Support ·
`mon_` Device Health · `int_` Integration/Sync · `rpt_` Reporting · `plat_` Platform.
**Never invent a variant** (`cust_`, `crm_`, `customer_`) for a domain that already has one.

## The sixteen binding rules

1. Every table is prefixed by its domain, from the list above.
2. Work only inside the domain you were asked to build. Do not modify global routing, layout, auth,
   the Supabase client, shared components, package files, or another domain's migrations without
   explaining why first.
3. **Never write to a table outside your domain's prefix.** No exception.
4. **Never read another domain's raw tables** — including from reporting and dashboard code. Use
   that domain's service client for operational reads, or an `rpt_*` view for reporting.
5. **Never use PostgREST nested or embedded selects across prefixes** — e.g.
   `.select('*, cl_locations(*)')` — even where a foreign key makes it available.
6. **Never open a database transaction spanning more than one prefix.**
7. Cross-domain effects are triggered by writing a row to `plat_domain_events`, **in the same
   transaction as the state change that caused it**. Event names are past-tense facts, not commands
   (`QuoteApproved`, `JobCreated`, `UnitReplaced`). Consumers must be idempotent — the same event
   delivered twice produces the same result.
8. Every table: `id uuid primary key default gen_random_uuid()`. Human-facing codes
   (`customer_code`, `quote_no`) are separate columns generated by a database function, never the
   primary key, allocated with proper locking — never max-plus-one.
9. Cross-domain FK columns are named `<prefix><entity>_id` (`cl_location_id`). Same-domain FKs are
   `<entity>_id` (`quote_id`).
10. **External system IDs never live in domain tables.** Airtable, HubSpot, Freshdesk and Xero IDs
    go only in `int_external_system_mappings` (system, entity_type, internal_id, external_id). This
    is what keeps the migration reversible.
11. **Snapshot** a value only when it must be frozen in time (quote pricing, contract customer name
    and address, delivery-note recipient). Otherwise store a **reference** so the value stays
    current. Test: *if the source record changes tomorrow, should this document read differently?
    No → snapshot, yes → reference.* Name snapshot columns to show it: `customer_name_snapshot`.
12. Every `rpt_*` view is created `WITH (security_invoker = true)`. **Not optional** — without it the
    view runs with the creator's rights and bypasses RLS on everything underneath.
13. The customer portal never reads a raw domain table, under any circumstance.
14. Cross-prefix foreign keys exist for referential integrity only, `ON DELETE RESTRICT`. They do
    not authorise joins or nested selects in application code — see rule 5.
15. Use shared status constants or enums for any status field. **Never write or compare a raw status
    string.** Create them before the first status field, not after the fourth. Quote status and quote
    approval state are two separate axes — a loose string conflates them.
16. **Before writing code, list the files you intend to create or edit and confirm they stay inside
    the current domain's boundary.**

## Security

The database enforces permissions; the client only reflects them. **A table without an RLS policy is
a bug, and CI catches it.**

- Every migration that creates a table creates its RLS policies **in the same file**.
- Three roles — admin, sales manager, user — stored in `plat_user_roles` and checked through a
  `SECURITY DEFINER` helper. **Never on the profile row** — that is privilege escalation by profile
  update.
- Per-role flags live in `plat_role_permissions` and resolve to a single `can(permission)` check.
  `can()` is **presentation only**; RLS is the actual control.
- **There are no implicit defaults.** A role–permission pair is granted or it is not. Admins do not
  fall back to allowed on an unset flag — that behaviour existed in the prototype and is a defect,
  not a feature.
- The privileged database client is importable only from server code, enforced by lint. The
  service-role key never reaches the browser.
- Every unauthenticated endpoint — webhooks, unsubscribe, cron targets — verifies a signature or
  signed token **before any write**.
- Every server input is parsed by a schema. Identifiers are regex-constrained, strings
  length-bounded.

## Never build

- **No development bypass.** No mode that grants every permission unconditionally, auto-signs-in a
  super-admin, or self-promotes one. It must not exist in any environment, dev included.
- **No client path that writes `plat_user_roles` or `plat_role_permissions` for the current user.**
  Roles are assigned by an admin through a server path.
- **No flag that skips signature or token verification**, in any environment.
- **No demo data seeding**, and no button that generates random records.
- **Never commit real or customer-shaped data.** `seed.sql` is synthetic.

## Runtime and migrations

Serverless functions forbid native binaries, child processes and long-lived TCP connections. Several
PDF, image and spreadsheet libraries are therefore unavailable — **check before reaching for one**,
and say so rather than substituting a library that cannot run.

Background work is queue tables in Postgres drained by a scheduled worker. There is no second
infrastructure component — no separate queue service, no long-running process.

Schema changes are **numbered SQL migrations committed to the repo** — including the ones Lovable
generates. Never a console edit.

## Visual rules

ConnectIQ is a compact, light-theme, data-forward operational B2B application: calm, precise,
trustworthy, lightly warm. Not a marketing site — avoid glassmorphism, oversized cards, giant
headings, empty hero sections, playful decoration, heavy shadows, excessive rounding and decorative
gradients. Project knowledge carries the token names, scales and pixel values; these are the rules
that hold whatever the values are. **They define style only** — never infer business logic, data
model, domain ownership, routes or component APIs from them.

- **Semantic CSS variables only.** Never a raw hex value or a framework palette class in feature
  code. A lint rule enforces this, not review.
- **Do not modify design tokens** unless the task is explicitly a design-system change. If a design
  needs a value the token set does not have, **state the missing token and its proposed semantic
  purpose before introducing it**.
- **Check the tokens before inventing** a colour, spacing step, radius, shadow or type size.
- **One theme per view.** Light unless the task names another. Never hand-invert light tokens to
  fake dark. Never bring the presentation theme's 16px body or 48–56px display type into an
  operational screen.
- **Purple is not a ConnectIQ colour.** Dashed purple in reference screenshots is Figma's selection
  indicator. The only purple is `chart.modelled`, meaning projected or forecast, allowed **only** as
  a chart series, legend entry or area fill — never on a badge, border, control or affordance.
- **Semantic hues keep one meaning each** across the whole application: green complete, yellow
  pending or scheduled, red error or incident, blue in progress or informational, orange brand,
  active navigation and focus. Orange is not the default filled primary.
- **Never communicate status with colour alone.** Text or an icon as well, every time.
- **No gradients** in chrome, forms, navigation, buttons, cards or dialogs. Subtle alpha gradients in
  charts only.
- **Preserve operational density.** Do not raise spacing or type toward marketing proportions, and do
  not solve narrow widths by shrinking all text.
- **Do not redesign unrelated pages** while implementing a feature.
- **Accessibility is not optional**: WCAG AA contrast, visible keyboard focus, labels connected to
  fields, accessible error messages, 44px effective touch targets on field workflows.
- Where a screenshot and a token disagree, **the token wins**.

## Stop rules

- If a milestone prompt names a blocking decision (`DR-nn`) that has not been answered, **stop and
  say so.** Do not pick the option that makes the code easier — every one of those decisions is cheap
  to make now and expensive to reverse after data lands.
- A merge conflict on a shared file (`package.json`, routing, layout, the Supabase client, auth)
  means something reached outside its domain. **Say so rather than resolving it.**
- If a feature appears to need a system, table or credential that project knowledge places outside
  the current phase, **stop and say so** — it is a scope change, not a task.
