Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .agents/skills/drizzle-database/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export const myModel = pgTable(
- `bigintAsString()` custom type: stores `bigint` in DB, exposes as `string` in app
- FK pattern: `.references(() => otherModel.id, { onDelete, onUpdate })`
- Index naming: `TableName_columnName_idx` or `TableName_column_key` for unique
- Every FK column must lead an index so RI cascade lookups do not scan child tables. `pnpm --filter @chatbotx.io/database test` runs `workspace-purge-coverage.test.ts`, which rejects any newly unindexed FK.
- A new table with `workspaceId` requires the `workspace-purge` skill. Partitioned-table indexes use the per-partition recipe in `20260924105732_add_contact_inbox_fk_indexes`.

### Enums

Expand Down Expand Up @@ -217,8 +219,9 @@ const item = await findOrFail({ table: myModel, where: { id } })
`Workspace.scheduledDeletionAt` is the repository's soft-delete convention:
use a nullable timestamp with an index. Scheduling sets it to `now + 24h`,
cancellation clears it, and the workspace row remains live during the grace
window. The hourly purge cron then hard-deletes due rows, allowing the normal
foreign-key cascade to remove children. `UserQuota.channelsTornDownAt` is the
window. The hourly purge cron drains large children in bounded batches before
hard-deleting the workspace; read `workspace-purge` before adding a
workspace-scoped table or teardown step. `UserQuota.channelsTornDownAt` is the
sibling marker for one-shot trial-expiry channel teardown; it is not a second
soft-delete state.

Expand Down
6 changes: 6 additions & 0 deletions .agents/skills/reliability-concurrency/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ Delete-then-insert (e.g. embedding `replaceForSource`) must be:

- Multiple agents editing the same working tree corrupt each other's git index. For parallel work use separate git worktrees.
- Stage specific files only — never `git add -A`/`.` (`.agents/rules/git.md`).
## 6. Bulk deletes and updates

- Bound every statement with a `LIMIT` and keyset or `FOR UPDATE SKIP LOCKED`.
- Commit each batch before starting the next; never build `IN` from every matching id.
- Set `lock_timeout` and `statement_timeout`, and keep the total run below its distributed-lock TTL.


## Stop condition

Expand Down
2 changes: 2 additions & 0 deletions .agents/skills/worker-development/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ quota, and tenancy lifecycle jobs are excluded from this gate.
### Adding a schedule handler

Use this four-edit flow, shown by the `purgeWorkspaces` handler:
When changing `purgeWorkspaces`, also read `workspace-purge` so the schedule remains bounded below its lock TTL.


1. Add the `ScheduleJobData` key, payload type, and union member in
`packages/worker-config/src/queues/schedule/index.ts`.
Expand Down
37 changes: 37 additions & 0 deletions .agents/skills/workspace-purge/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
name: workspace-purge
description: Keep workspace deletion bounded, indexed, resumable, and safe under lock contention.
---

# Workspace Purge Safety

Use this skill for a new table, foreign key, workspace-scoped data, hypertable, or any change to workspace deletion, purge, or runtime freeze.

## Lifecycle

`scheduleDeletion` (reversible grace period) → worker `purgeWorkspaces` →
`workspaceService.teardownDueWorkspace`:

1. acquire the workspace purge lease;
2. freeze runtime work and disconnect integrations/channels only after deletion is due;
3. drain workspace analytics, heavy tables, partitioned rows, and `ContactInboxPost`;
4. delete `Workspace` with bounded timeouts, then release quota;
5. reconcile the owner's correct quota scope.

The scheduled worker may retry this path. Every step must be idempotent and leave incomplete work for the next tick through `WorkspacePurgeIncompleteError`.

## Required rules

1. **Every new foreign-key column needs a leading index.** A parent delete invokes a child RI lookup for every deleted parent row. Nullable `SET NULL` foreign keys use a partial `WHERE column IS NOT NULL` index. The CI-enforced `packages/database/__tests__/workspace-purge-coverage.test.ts` rejects a newly unindexed FK; `purge-fk-indexes.test.ts` verifies migration shape for high-volume paths.
2. **Drain large workspace tables before the final cascade.** A log, event, stat, per-contact, or per-message table with `workspaceId` belongs in `repositories/workspace-purge/tables.ts`, in child-before-parent order, and needs a leading `workspaceId` index. Partitioned tables delete by their primary-key tuple with a per-table workspace selector; never use `ctid`. Hypertables delete bounded workspace time slices with `liftDecompressionLimit`, shrinking the slice after a timeout.
3. **Delete workspace-scoped tables without a foreign key explicitly.** A missing FK does not make data disappear at workspace deletion. Add the table and owning strategy to `NO_FK_WORKSPACE_TABLES`; the coverage test fails if a table with `workspaceId` is neither cascade-reachable nor registered.
4. **Keep every purge/freeze statement bounded.** Use a `LIMIT` plus keyset or `FOR UPDATE SKIP LOCKED`, one short transaction per batch, and no `IN` built from every matching id. Never call HTTP while holding a database transaction. Use `withPurgeTimeouts`; stop before the cron deadline so its run stays below the distributed-lock TTL.
5. **Make teardown idempotent.** It runs again on each tick. Convert temporary lock/timeout failures into `WorkspacePurgeIncompleteError` so the next tick resumes safely.
6. **Reconcile the correct quota scope.** Call `userQuotaService.reconcileOwnerAfterTeardown(ownerId)`; never count all root-tenant resources as a reseller pool.
7. **Retain billing ledgers.** Owner-keyed billing consumption rollups such as `WorkspaceMac` are evidence of period usage and must be registered as `retainedBilling`, never deleted with their workspace.

## Before merge
- Run `pnpm --filter @chatbotx.io/database test` and inspect the purge FK index tests.
- Inspect generated migration SQL. Partitioned indexes must use per-partition `CREATE INDEX CONCURRENTLY`, `ON ONLY` parent indexes, `ATTACH PARTITION`, and validity checks.
- Run `EXPLAIN (ANALYZE, BUFFERS)` in `BEGIN … ROLLBACK` for representative purge batches. RI triggers must use index scans.
- Verify a purge alongside activity in another workspace: no long-lived lock waits and no purge transaction longer than a batch.
5 changes: 3 additions & 2 deletions .claude/agents/invariant-guard.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: invariant-guard
description: Use PROACTIVELY after any edit under apps/, packages/, or integrations/ to check a diff against the non-obvious ChatbotX invariants that no lint rule enforces (triple-d middleware names, relations/index.ts double-edit, ChannelType cascade, bindArgsSchemas binding, no-input execute(), i18n-mandatory, Drizzle-not-Prisma, no direct db in app layer, no dynamic import, new-package CI install, flow node type cascade, TanStack Query mutation invalidation, agent-first public API parity). Reports violations only; does not edit.
description: Use PROACTIVELY after any edit under apps/, packages/, or integrations/ to check the non-obvious ChatbotX invariants, including workspace purge safety. Reports violations only; does not edit.
tools: Read, Grep, Glob, Bash
model: sonnet
---
Expand Down Expand Up @@ -30,6 +30,7 @@ You are a focused reviewer with ONE job: catch violations of the ChatbotX invari
11. **Flow node type cascade.** If `nodeTypeSchema` in `packages/flow-config/src/nodes/base.ts` gained a value, grep an existing node schema symbol (e.g. `waitNodeSchema`) and `nodeTypeSchema.enum.` across `apps/` and `packages/` — the new node must appear in the `flowVersionSchema` union (`packages/flow-config/src/nodes/index.ts`), `allNodesConfig` (`nodes/node-config.tsx`), `allSteps` (`steps/index.tsx`), `viewerNodeTypes` (`react-flow-wrapper.tsx`), and `analyticsNodeTypes` (`node-types-config.ts`). These are plain-object maps and hand-rolled unions that fail SILENTLY (node doesn't render, or publish fails with a generic toast). Also flag any NEW zod union that hand-lists node schemas instead of reusing `flowVersionSchema`/`publishFlowSchema`.
12. **TanStack Query mutation invalidation.** If a mutation creates, updates, deletes, toggles, or moves a resource read through a TanStack Query hook, flag a missing resource invalidator (or `setQueryData`) in its success path; `router.refresh()` alone does not update the browser cache. Also flag a new `ChangeFolderDialog` caller that omits `onSuccess`, because the dialog cannot know which resource key to invalidate.
13. **Agent-first public API parity** (AGENTS.md invariant 23). If the diff adds a server action under `apps/builder/src/features/*/actions/`, a procedure in a feature's `api/private.ts`/`api/index.ts`, or a new feature folder, check that the same feature's `api/public.ts` (or `api/public/*.ts`) gains — or already has — a procedure for the same operation, and that it is nested in `apps/builder/src/routers/public.ts`. Pay extra attention to result-returning operations: an action or procedure whose name matches `preview|test|simulat|generate|export|run|sync|scan|stats|link|render` is the kind most often shipped UI-only. Also flag a public procedure that re-implements logic instead of calling the same service/`lib/` function as the UI path. Skip features on the invariant 23 exempt list (auth/account, platform admin, end-customer webviews, OAuth redirect step, UI-only plumbing, token minting) but say which exemption you applied. Severity: HIGH (the operation is invisible to the CLI/MCP).
14. **Workspace purge safety.** If the diff adds or changes a schema FK, a workspace-scoped table, `workspace-lifecycle`, workspace purge code, campaign cleanup, or dispatch cancellation, require the `workspace-purge` rules: leading FK indexes; child-before-parent bounded drains; PK-tuple deletes for partitioned data; chunk drains for hypertables; short timeout-guarded transactions. Require `packages/database/__tests__/workspace-purge-coverage.test.ts` for a new workspace-scoped table or FK.

## Output

Expand All @@ -38,5 +39,5 @@ A single findings table, severity-tagged (HIGH = silent runtime/clone breakage;
## Boundaries

- Read-only. Never edit files. Report only.
- Do not review general code quality, style, or logic — that is other reviewers' job. Stay on these 13 invariants.
- Do not review general code quality, style, or logic — that is other reviewers' job. Stay on these 14 invariants.
- If a diff touches none of the invariant surfaces, return `INVARIANTS: PASS (no invariant-relevant changes)` and stop.
2 changes: 2 additions & 0 deletions .devin/rules/chatbotx.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ pnpm workspaces + Turborepo · TypeScript 5 · React 19 · Next.js 16 (builder)
22. **`ContactInboxPost` has no FK and is partitioned by `workspaceId`.** Every ContactInbox, Contact, or Workspace delete path must go through `contactInboxPostService`; every post query must include `workspaceId`; and post writes must hold the durable workspace purge guard through commit.

23. **Agent-first: every feature ships a public API — including the ones that only return a result.** The CLI and MCP server derive their whole surface from `GET /api/public-spec.json` (`publicRouter`), so a capability without a public procedure does not exist for an AI agent, however well it works in the UI. A new feature — or a new operation on an existing one — must add a procedure to its `features/<x>/api/public.ts`, nested in `apps/builder/src/routers/public.ts`, behind `workspaceTokenAuthAPIForScope("<scope>")`. This covers **result-returning operations**, not just CRUD: anything a user clicks in the UI to *get something back* — a link (`botSimulator.getLink` is the reference), a preview or render, a test run/test send/connection test, stats and reports, an export, AI-generated content, a status check, a sync or scan. The rule of thumb: if a button in the UI produces a result, an agent must be able to call one operation and receive the same result. Build the public procedure **first** and the UI second; both call the same service method or `features/<x>/lib/` function, never two implementations. The response must be machine-usable data (ids, URLs, JSON) — never something that only renders on screen — and the procedure's `summary`/`description` must say which operation supplies each input id (e.g. "Get webchat ids from `webchats.list`"). Then run the `cli-mcp-docs` skill. **Exempt** (state the reason in the PR): sign-in/sign-up and personal account settings; platform administration (super admin, reseller, tenant, platform credentials); end-customer public pages (webviews, short links, booking pages); the browser-redirect step of a channel OAuth connect (the post-connect read/settings/disconnect still need an API); UI-only plumbing (device push tokens, realtime, help items); and operations a workspace token must never perform (minting/revoking API tokens).

24. **Workspace purge must stay bounded.** Every new FK must have a leading index; high-volume workspace purge paths need explicit audit coverage. `packages/database/__tests__/workspace-purge-coverage.test.ts` rejects a workspace-scoped table without a cascade path or registered purge strategy, and new unindexed FKs. Large workspace tables drain child-before-parent in bounded batches; partitioned tables delete by PK tuples and hypertables by workspace chunks; freeze/purge work uses short timeout-guarded transactions. Billing consumption ledgers (`WorkspaceMac`) are owner-keyed and are never purged with the workspace. Read `.agents/skills/workspace-purge/SKILL.md`.
<!-- END GENERATED: SHARED-INVARIANTS -->

## Workflow
Expand Down
2 changes: 2 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ These instructions apply repository-wide. Read the relevant detailed rule in `.a
22. **`ContactInboxPost` has no FK and is partitioned by `workspaceId`.** Every ContactInbox, Contact, or Workspace delete path must go through `contactInboxPostService`; every post query must include `workspaceId`; and post writes must hold the durable workspace purge guard through commit.

23. **Agent-first: every feature ships a public API — including the ones that only return a result.** The CLI and MCP server derive their whole surface from `GET /api/public-spec.json` (`publicRouter`), so a capability without a public procedure does not exist for an AI agent, however well it works in the UI. A new feature — or a new operation on an existing one — must add a procedure to its `features/<x>/api/public.ts`, nested in `apps/builder/src/routers/public.ts`, behind `workspaceTokenAuthAPIForScope("<scope>")`. This covers **result-returning operations**, not just CRUD: anything a user clicks in the UI to *get something back* — a link (`botSimulator.getLink` is the reference), a preview or render, a test run/test send/connection test, stats and reports, an export, AI-generated content, a status check, a sync or scan. The rule of thumb: if a button in the UI produces a result, an agent must be able to call one operation and receive the same result. Build the public procedure **first** and the UI second; both call the same service method or `features/<x>/lib/` function, never two implementations. The response must be machine-usable data (ids, URLs, JSON) — never something that only renders on screen — and the procedure's `summary`/`description` must say which operation supplies each input id (e.g. "Get webchat ids from `webchats.list`"). Then run the `cli-mcp-docs` skill. **Exempt** (state the reason in the PR): sign-in/sign-up and personal account settings; platform administration (super admin, reseller, tenant, platform credentials); end-customer public pages (webviews, short links, booking pages); the browser-redirect step of a channel OAuth connect (the post-connect read/settings/disconnect still need an API); UI-only plumbing (device push tokens, realtime, help items); and operations a workspace token must never perform (minting/revoking API tokens).

24. **Workspace purge must stay bounded.** Every new FK must have a leading index; high-volume workspace purge paths need explicit audit coverage. `packages/database/__tests__/workspace-purge-coverage.test.ts` rejects a workspace-scoped table without a cascade path or registered purge strategy, and new unindexed FKs. Large workspace tables drain child-before-parent in bounded batches; partitioned tables delete by PK tuples and hypertables by workspace chunks; freeze/purge work uses short timeout-guarded transactions. Billing consumption ledgers (`WorkspaceMac`) are owner-keyed and are never purged with the workspace. Read `.agents/skills/workspace-purge/SKILL.md`.
<!-- END GENERATED: SHARED-INVARIANTS -->

## Rule index
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ These are the most common mistakes — read before writing any code:

23. **Agent-first: every feature ships a public API — including the ones that only return a result.** The CLI and MCP server derive their whole surface from `GET /api/public-spec.json` (`publicRouter`), so a capability without a public procedure does not exist for an AI agent, however well it works in the UI. A new feature — or a new operation on an existing one — must add a procedure to its `features/<x>/api/public.ts`, nested in `apps/builder/src/routers/public.ts`, behind `workspaceTokenAuthAPIForScope("<scope>")`. This covers **result-returning operations**, not just CRUD: anything a user clicks in the UI to *get something back* — a link (`botSimulator.getLink` is the reference), a preview or render, a test run/test send/connection test, stats and reports, an export, AI-generated content, a status check, a sync or scan. The rule of thumb: if a button in the UI produces a result, an agent must be able to call one operation and receive the same result. Build the public procedure **first** and the UI second; both call the same service method or `features/<x>/lib/` function, never two implementations. The response must be machine-usable data (ids, URLs, JSON) — never something that only renders on screen — and the procedure's `summary`/`description` must say which operation supplies each input id (e.g. "Get webchat ids from `webchats.list`"). Then run the `cli-mcp-docs` skill. **Exempt** (state the reason in the PR): sign-in/sign-up and personal account settings; platform administration (super admin, reseller, tenant, platform credentials); end-customer public pages (webviews, short links, booking pages); the browser-redirect step of a channel OAuth connect (the post-connect read/settings/disconnect still need an API); UI-only plumbing (device push tokens, realtime, help items); and operations a workspace token must never perform (minting/revoking API tokens).

24. **Workspace purge must stay bounded.** Every new FK must have a leading index; high-volume workspace purge paths need explicit audit coverage. `packages/database/__tests__/workspace-purge-coverage.test.ts` rejects a workspace-scoped table without a cascade path or registered purge strategy, and new unindexed FKs. Large workspace tables drain child-before-parent in bounded batches; partitioned tables delete by PK tuples and hypertables by workspace chunks; freeze/purge work uses short timeout-guarded transactions. Billing consumption ledgers (`WorkspaceMac`) are owner-keyed and are never purged with the workspace. Read `.agents/skills/workspace-purge/SKILL.md`.

<!-- END GENERATED: SHARED-INVARIANTS -->

## Git conventions
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
| After any public API change (every one surfaces in the CLI/MCP), or CLI/MCP docs/skill drift | `cli-mcp-docs` |
| Business logic, new service method, any DB read/write from app code | `business-data-access` |
| New DB table or migration | `drizzle-database` |
| New table / FK / workspace-scoped data, or any change to workspace deletion/purge/freeze | `workspace-purge` |
| New background job or queue | `worker-development` |
| New channel integration | `integration-channel` |
| Contact filter field/operator, filter SQL, or contact-based audience | `contact-filter` |
Expand Down
9 changes: 9 additions & 0 deletions apps/builder/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- BEGIN:nextjs-agent-rules -->

# This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.

This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.

<!-- END:nextjs-agent-rules -->
Loading
Loading