diff --git a/docs/superpowers/specs/2026-05-27-productize-tradyon-procurement-design.md b/docs/superpowers/specs/2026-05-27-productize-tradyon-procurement-design.md new file mode 100644 index 0000000..57462be --- /dev/null +++ b/docs/superpowers/specs/2026-05-27-productize-tradyon-procurement-design.md @@ -0,0 +1,314 @@ +# Productize Tradyon Procurement — Design Spec + +**Date:** 2026-05-27 (revised 2026-05-28 after staff-engineer review) +**Author:** Siddhant Mundra + Claude (brainstorming session, revised after review) +**Status:** Approved — ready for execution planning + +--- + +## 1. Goal + +Take the current Tradyon Procurement prototype (the codebase at `Sidgit11/ProcurementManager`, deployed at `procurement-manager.vercel.app`) and turn it into a real, multi-tenant SaaS product usable day-to-day by **3–5 design partners** in the Polico ICP band (mid-market commodity traders, processors, importers). + +**Audience for v1:** 3–5 design partners, hand-onboarded but using a fully self-serve Clerk sign-up flow. No billing — Tradyon eats infrastructure cost during the design phase. + +**Bar:** "Lean SaaS" reliability — partners can run real procurement on it, but no formal SLA, no SOC2. + +**Engineering team:** Siddhant + Claude (Claude executes, Siddhant steers + reviews). Estimate calibrated for that pairing. + +--- + +## 2. Critical constraint: do not break the demo + +The current deployment at `procurement-manager.vercel.app` is actively used for sales demos. **It must remain bit-for-bit untouched** through this entire effort. + +**Isolation strategy:** new git repo entirely. + +- New repo: `TradyonProcurementProd` (private). +- Old repo (`Sidgit11/ProcurementManager`) is frozen as the demo. Branch protection on `main`. README banner pointing to the new prod repo. +- New Vercel project linked to the new repo, with its own Neon Postgres DB, its own Clerk application, its own Anthropic API key, its own domain (`app.tradyon.ai`). +- No code or infrastructure is shared between demo and production. +- **Cherry-pick policy:** the demo gets *no* commits from the prod repo, full stop, except a single README banner update. Future improvements to the demo (cosmetic tweaks for sales) are ported manually if and only if explicitly requested. + +The staff-engineer review flagged the porting tax of this decision (every shared component diverges, two CI configs, two dependabot streams). Acknowledged and accepted: demo stability is non-negotiable for sales. + +--- + +## 3. Scope + +The scope is split into **Tier 1** (load-bearing for daily partner use — must be production-grade) and **Tier 2** (must not regress, polish deferred to v1.1). + +### 3.1 Tier 1 — must work for daily partner use + +**Ingestion + processing** +- Gmail OAuth ingestion (per-user tokens, see §4.3). +- WhatsApp Cloud API ingestion + send (Meta verified; Periskope kept as warm-standby fallback). +- WhatsApp Chat Export upload (already works). +- Anthropic-based quote extraction (demo regex extractor removed). + +**Daily-use pages** +- `/digest` +- `/inbox` + `/inbox/[vendorId]` +- `/compare` +- `/vendors` + `/vendors/[id]` +- `/rfq/new` + `/rfq/[id]` +- `/opportunities` + `/opportunities/[id]` +- `/po/[id]` +- `/alerts` +- `/notifications` +- `/settings/*` (specifically `/settings/integrations` for managing Gmail + WhatsApp connections) +- `/onboarding` (first-run wizard — see §4.5.1) + +**Infrastructure** +- Multi-tenant with Clerk fully on (`DEMO_MODE` removed). +- 4 LLM agents in propose-only mode: daily summary, buy-now scanner, follow-up nudges, negotiation drafter. +- Lean SaaS ops: Sentry, Better Stack uptime, Neon backups + PITR, structured logs, runbook. +- Unit + integration tests (target ~70% coverage on `src/lib/`), CI on PR. +- **Static `org_id` lint rule in CI from Phase 0** (see §4.7). + +### 3.2 Tier 2 — must not break, polish deferred to v1.1 + +These pages stay in the app and continue to work, but receive only bug-fix attention during this effort. Visual polish, performance tuning, and feature gaps are explicitly deferred to v1.1. + +- `/insights/forecasts` (the page works; model-picker save is a v1.1 polish) +- `/insights/workshop` +- `/agents` (existence verified, full audit-log UI is v1.1) +- `/ask` (streaming + tool-calling works; deeper org-scoped tool coverage is v1.1) +- `/vendors` Discover tab (curated list works; shipment-data integration deferred) +- `/vendors/[id]/vault` + +### 3.3 Out of scope (explicit, in writing) + +- Voice-note transcription (Whisper). Voice messages show in the inbox as a link back to WhatsApp. +- Outlook OAuth ingestion. +- Real shipment-data vendor discovery (Importgenius / Panjiva / Volza). +- Billing / Stripe. +- Inbound supplier PDF OCR (PDFs go to vendor vault for manual review). +- SOC2 / formal compliance. +- Mobile app. +- Gmail push notifications via Pub/Sub (polling for v1; push is v1.1). +- Demo → prod data import (partners start clean — see §4.8). + +--- + +## 4. Architecture overview + +### 4.1 Bootstrap + +| Concern | Choice | +|---|---| +| Git host | GitHub, new private repo | +| Initial commit | Squash of `Sidgit11/ProcurementManager@98f3224` so the new repo starts clean | +| Hosting | Vercel (new project) | +| DB | Neon Postgres via Vercel Marketplace (new project), separate Neon project | +| Auth | Clerk (new Clerk application under same Clerk org) | +| LLM | Anthropic, dedicated API key for the new project, project-level monthly spend cap | +| Domain | `app.tradyon.ai` (with `*.vercel.app` as bootstrap) | +| Error tracking | Sentry | +| Uptime | Better Stack | +| Cron | Vercel Cron (1-minute minimum on Pro tier; we use 1-min, never 30-sec) | +| WhatsApp primary | Meta Cloud API (already verified) | +| WhatsApp fallback | Periskope adapter (kept warm, swappable via env flag) | + +### 4.2 Multi-tenancy + +- `DEMO_MODE=1` removed from the Vercel project. All demo-mode branches (`isDemo()`, `currentOrg()` fallback, `scan.ts` early return, `demo-extract.ts`, `upload/route.ts` synchronous tick) are deleted; the real path is the only path. +- Clerk sign-up flow: user creates an account → Clerk webhook hits `/api/clerk/webhook` (signature-verified) → creates a row in `org` and `user`. The current webhook handler exists but is stubbed; needs real implementation. +- Schema is already multi-tenant from day one (every business table has `org_id`). +- **Runtime check (necessary, not sufficient):** vitest helper that seeds two orgs (`orgA`, `orgB`), runs each Tier 1 page's data query as the other org's `currentOrg()`, asserts no rows from the first org appear. +- **Static check (the strong guarantee):** CI job runs an ast-grep / ESLint rule that fails any `db.select()`, `db.update()`, `db.delete()`, or `db.execute(sql\`…\`)` call whose surrounding query does not include an `org_id` filter or an explicit `// org-scoped: ` annotation. Lives in Phase 0 so every PR from Phase 1 onward is guarded. + +### 4.3 Ingestion + +**Gmail OAuth (per-user tokens)** + +Token scoping decision: **per-user**. Each teammate connects their own Gmail. RFQs sent from a user's Gmail show `From: their.address@…`. When a user leaves the org, their `gmail_connection` row is preserved (thread history stays linked to the org) but RFQ-send-as-them is disabled. + +- New table: + ``` + gmail_connection { + id, org_id, user_id, email, + access_token_enc, refresh_token_enc, key_version, + history_id, scopes[], + status, -- "active" | "needs_reauth" | "revoked" + last_synced_at, + last_error, last_error_at + } + ``` +- Encryption: libsodium with project-scoped secret `GMAIL_TOKEN_KEY` in Vercel env, marked Sensitive. +- **Key rotation procedure (documented in runbook):** generate `GMAIL_TOKEN_KEY_v2`, deploy with both keys readable, mark new connections as `key_version=2`, lazy-re-encrypt v1 rows on next access, remove v1 key after the longest possible re-auth window (90 days). `key_version` column makes the dual-read window safe. +- **Refresh-token failure UX:** when Google invalidates the refresh token (silent after 6 months inactivity or password change), the poller catches `invalid_grant`, sets `status='needs_reauth'`, creates an in-app notification, and surfaces a yellow banner on every page until reconnected. RFQ-send button disabled with clear copy: *"Reconnect Gmail to send."* +- New endpoints: `POST /api/auth/gmail/start`, `GET /api/auth/gmail/callback`, `POST /api/auth/gmail/disconnect`. Connect button lives in `/settings/integrations`. +- Pull strategy: Vercel Cron every 1 minute runs `/api/cron/gmail-poll`. Per active connection, calls Gmail History API with `historyId`. Upserts new `message` + `thread` rows. Enqueues `extraction_job` for each new message. +- **Edge cases handled:** `historyId` expiry (>1 week stale → full INBOX rescan, bounded 90 days), label scoping (only INBOX + `quoted-replies` labels, configurable), partial-sync resumption, dedup by Gmail `message-id` header. +- First connect does a bounded 90-day historical sync. +- Outbound: RFQ send routes through the user's Gmail OAuth token. + +**WhatsApp** +- Env-gated provider: `WHATSAPP_PROVIDER=cloud|periskope`. Cloud is the production path (already verified). Periskope is kept implemented and warm so we can flip with one env-var change if Meta has an outage or policy change. +- Adapter interface: `WhatsappAdapter { sendMessage(input), verifyWebhook(req), parseWebhook(req) }`. Both Cloud and Periskope implementations live behind it. Same integration tests run against both. +- Webhook: `POST /api/webhooks/whatsapp`. Signature verified per provider. +- Inbound messages upserted to `message` + `thread`, extraction enqueued. +- Outbound: RFQ + nudge messages sent via the active provider. +- WhatsApp Chat Export upload: unchanged from the current prototype. + +**Extraction** +- `demo-extract.ts` deleted. Anthropic-based `extract.ts` (already exists in the prototype) becomes the only path. +- Cost guard: per-org daily budget in `org.settings.anthropicDailyBudgetUsd` (default $5). Soft-fail (queue + warn the org owner via in-app notification) when exceeded. +- Job runner: `extraction_job` table exists. Move from inline `tickOnce(200)` (the demo upload pattern) to a background cron `/api/cron/extraction-tick` every 1 minute (Vercel Pro minimum). +- **Cron platform reality:** Vercel Cron minimum is 1 minute, not 30 seconds. Accepted 1-minute extraction latency. If sub-minute latency becomes a partner ask, migrate to QStash or Inngest in v1.1. + +### 4.4 Agents + +All four agents run in **propose-only mode for v1** — the `auto_execute` column on `agent_policy` stays `false` until a partner explicitly opts in. + +| Agent | Trigger | Output | Failure mode | +|---|---|---|---| +| Daily summary | Cron 06:00 BRT per org, **jittered by `hash(org_id) mod 30 min`** | Writes `agent_run.proposed_actions.summary` rendered on `/digest` hero. Uses Claude Haiku for cost. | UI falls back to the existing computed fallback string — no breakage. | +| Buy-now scanner | Cron 07:00 BRT per org, jittered | Creates `buy_opportunity` rows. Idempotent (dedup by `quote_id`). Strip the `isDemo()` short-circuit in `scan.ts`. | No-op on Anthropic error; logged + retried next run. | +| Follow-up nudges | Cron 09:00 BRT per org, jittered | Finds `rfq.status='sent'` with no response after N=3 days. Drafts a nudge into `agent_run.proposed_actions.draft`. Creates a `notification`. **User reviews and sends — no auto-send.** | Skip individual RFQ on draft failure; log. | +| Negotiation drafter | Triggered on `buy_opportunity` create with `status='open'` | Drafts a counter-offer linked to the opportunity. **User reviews and sends.** | Skip on draft failure; opportunity remains actionable manually. | + +**Cron jitter:** without staggering, 5 orgs all firing at 06:00 BRT will hit Anthropic rate limits as a thundering herd. Each cron's effective fire time is `nominal + (hash(org_id) % 30 minutes)`. Implemented as a leader cron that schedules per-org work. + +**Agent quality signals (new):** add `agent_run.quality_signals jsonb` capturing `{ draft_sent: bool, draft_edited_pct: number, time_to_send_seconds: number, user_dismissed: bool }`. This is the only objective signal that the agents are useful. Surfaced on the `/agents` Tier 2 page as a v1.1 polish item, but the *data capture* lands in Phase 4. + +Vendor-discovery agent: deferred. Discover tab keeps the curated candidate list (`DiscoverGrid.tsx`). + +### 4.5 Feature-surface hardening (Tier 1) + +| Page | Hardening | +|---|---| +| `/digest` | Empty-state when org has 0 quotes ("we're listening — first quotes appear here within minutes of connecting Gmail"). | +| `/inbox` | Pagination at >200 threads. Read-state via new `message_read` join table. Empty-state. | +| `/inbox/[vendorId]` | Mark messages read on open. Optimistic UI when sending reply. | +| `/compare` | Date-range filter (last 30/90/180 days). Empty-state per SKU. | +| `/vendors` | "Add vendor" + curated Discover already shipped. Empty-state on My Pool. | +| `/vendors/[id]` | Products card already shipped. Guard against last-primary-contact deletion. | +| `/rfq/new` | Template interpolation shipped. **Real send via Gmail OAuth (for email) and WhatsApp adapter (for WA) shipped in Phase 2 + 3, not Phase 5.** | +| `/rfq/[id]` | Status flips when vendor replies on the same thread (matched via `In-Reply-To` header + thread-id). | +| `/opportunities` + `/opportunities/[id]` | Buy → real PO creation. Negotiate → fires negotiation agent draft. | +| `/po/[id]` | **Email-the-PO button via Gmail OAuth ships in Phase 2** (with the rest of Gmail send), not Phase 5. | +| `/alerts` | Real cron-based evaluation. Trigger sends email + in-app notification. | +| `/notifications` | Mark-read endpoint. Polling for v1 (no Pusher). | +| `/settings/*` | Org settings (currency, home port), profile, **integrations panel (Gmail per-user + WhatsApp org-level connection management)**, Anthropic budget. | + +### 4.5.1 Onboarding wizard (hardened, "make-or-break") + +The first-run experience determines whether 3–5 design partners stick. It gets the same hardening rigor as ingestion. + +**Steps** +1. **Welcome** — org name confirmation (came from Clerk sign-up), explainer of what happens next. +2. **Connect Gmail** — single-button OAuth, clear permission scope explainer (we only read INBOX + send-as), retry on failure with explicit error copy. +3. **Connect WhatsApp** *(optional)* — choose: Cloud API number (Tradyon-issued) or upload existing Chat Export. Chat Export sample download link. +4. **Org defaults** — currency (default USD), home port (default `BR-NVT`), preferred channels. +5. **Catalog confirmation** — present the 7 Polico-default SKUs, partner can keep or replace with their own SKUs. +6. **Tour** — 4-step interactive walkthrough of `/digest`, `/inbox`, `/compare`, `/rfq/new` with empty-state copy at every step. "We're listening — first quotes appear here within minutes." + +**State management** +- `org.settings.onboardingStep` persists current step. Partners can resume. +- Each step writes a row to `event_log` for funnel analysis. +- Skip-and-come-back is allowed for every step except step 2 (Gmail). Step 2 can be deferred to step 6, but the tour explicitly tells them they'll see nothing until Gmail is connected. + +**Empty states for every Tier 1 page** when org has no data: same copy library, accessible via `` component. Reasons: `no-quotes-yet`, `gmail-not-connected`, `no-opportunities-yet`, `no-rfqs-yet`, `no-alerts-yet`. + +### 4.6 Logging & observability + +A first-class concern, not an afterthought. + +- **Structured JSON logs** via a single `src/lib/log.ts` wrapper. Every log line carries `{ timestamp, level, requestId, orgId, userId, route, msg, ...ctx }`. Levels: `debug | info | warn | error`. +- **Request correlation**: middleware mints a `requestId` per request, attaches to async-local-storage so any deep call site can include it. +- **Server-side context**: `currentOrg()` populates `orgId` into the ALS context — no log line should ship without it. +- **Log destinations**: Vercel Functions logs are the default. The wrapper is provider-agnostic so a Datadog/Axiom/Logflare swap is one file. +- **Key call sites with explicit logging**: + - Gmail poll cron (per-connection success/failure, page count, message count, `historyId` advance) + - WhatsApp webhook (signature verify, parsed payload shape, dropped messages) + - Extraction job tick (queue depth, jobs/sec, Anthropic latency, token cost) + - Each agent run (start, end, error, cost, rows created, **`quality_signals`**) + - Clerk webhook (org create, user create, errors) + - Auth failures (`currentOrg()` throw paths) + - **Key rotation events** (`GMAIL_TOKEN_KEY` re-encrypt operations) +- **Sentry** for unhandled exceptions on server + client. +- **Health endpoint** `GET /api/health` returns `{ db, anthropic, lastCronRun }` for Better Stack to poll every minute. +- **Cron health page** (`/admin/cron-health`) surfaces last 7 runs of each cron job from a new `cron_run` table. + +### 4.7 Testing + +- **Unit tests** with vitest. Every new module ships with tests; target ~70 % line coverage on `src/lib/`. CI fails PRs below target. +- **Integration tests** for ingestion adapters: Gmail adapter against a recorded fixture mailbox; WhatsApp adapter against a recorded webhook payload set; both provider implementations (Cloud + Periskope) against the shared adapter interface. +- **Multi-tenant isolation tests (runtime):** vitest helper that seeds two orgs (`orgA`, `orgB`), runs each Tier 1 page's data query as the other org's `currentOrg()`, asserts no rows from the first org appear. +- **Multi-tenant isolation guard (static, in CI from Phase 0):** ast-grep or ESLint rule that fails any `db.select()/db.update()/db.delete()/db.execute(sql\`…\`)` call whose surrounding query lacks an `org_id` filter or an explicit `// org-scoped: ` annotation. This is the strong guarantee. +- **Agent tests**: each agent module is unit-tested against a fixture DB snapshot. Output draft is asserted on shape (not content). `quality_signals` write paths tested. +- **CI**: GitHub Actions runs `pnpm tsc --noEmit && pnpm lint && pnpm vitest run` on every PR. Coverage gate enforced. +- The existing 39 prototype tests carry over to the new repo as the starting baseline. + +### 4.8 Reliability, security & ops (Lean SaaS bar) + +- **Sentry**: server + client SDK. Source maps via Vercel integration. Alerts → email/Slack on new issues. +- **Uptime**: Better Stack monitoring `https://app.tradyon.ai/api/health`. 1-min interval, paging webhook to email. +- **Backups**: Neon daily PITR enabled. Manual snapshot procedure in `scripts/db-snapshot.ts`. +- **Runbook**: `docs/operations/runbook.md` covering Gmail token expired/invalid_grant, Anthropic 429, WhatsApp signature mismatch, Clerk webhook race, cron stuck, DB connection exhaustion, `GMAIL_TOKEN_KEY` rotation procedure. Each entry has the exact diagnosis command + fix. +- **Cron health**: `cron_run { id, job_name, started_at, finished_at, status, error, ctx }` rows + the admin page mentioned above. +- **Secrets**: all in Vercel encrypted env vars. Anthropic key, Gmail client secret, WhatsApp tokens, `ADMIN_SECRET`, `GMAIL_TOKEN_KEY` marked Sensitive in Vercel UI. +- **Schema migration safety policy** *(new)*: + - Drizzle migrations are forward-compatible only — no destructive renames, no required-column adds without a default. + - `drizzle-kit push` is forbidden in prod; only `drizzle-kit migrate` against a Neon staging branch first. + - Every migration is tested on a Neon branch that's a fresh fork of prod data before being applied to prod. + - Rollback policy: every migration has a documented "how to undo" comment in the migration file. If undoing is destructive, the migration is rewritten as additive. +- **Partner offboarding / data deletion *(new)*:** `scripts/delete-org.ts` script. Takes an `org_id`, deletes from every business table via cascade. Preserves `event_log` rows (anonymized) for compliance audit. Documented in runbook. +- **Supplier-side privacy *(new)*:** ingested supplier emails contain third-party PII. The privacy policy on `app.tradyon.ai/legal/privacy` explicitly names this, the lawful basis (legitimate interest), and the retention policy (delete on partner offboarding via the script above). One-page legal doc, drafted in Phase 6. + +--- + +## 5. Phasing + +**Total: 13 weeks** (12 weeks of dev + 1 week buffer), Siddhant + Claude. + +Estimate revised up from the original 10–12 weeks after staff-engineer review: +- Gmail OAuth + History API edge cases (Phase 2): 2 → 3 weeks +- WhatsApp dev work shorter because Meta verification is already done +- Phase 5 shorter because "real send" moved into Phases 2 + 3 +- Phase 1 grew slightly to absorb the Clerk webhook + static-isolation work +- 1 week of buffer at the end for things that always slip + +| Phase | Weeks | Outcome | +|---|---|---| +| 0. Bootstrap + isolation safety | 1.5 | New repo, Vercel, Neon, Clerk, domain. CI passes. Logging + Sentry skeleton in place from day one. **Static `org_id` lint rule in CI.** WhatsApp Cloud verification confirmed (done). Periskope adapter scaffold landed as warm fallback. Domain DNS + SSL working. | +| 1. Auth + multi-tenancy | 1.5 | DEMO_MODE removed. Clerk sign-up creates org + user via signature-verified webhook. Two test orgs verify isolation (runtime + static checks). Empty-state component library landed. | +| 2. Gmail ingestion + send | 3 | Per-user OAuth flow with `key_version` + rotation procedure. History-API polling (1-min cron). Message + quote insertion. `needs_reauth` banner UX. **`/rfq/new` and `/po/[id]` real Gmail send shipped in this phase.** Integration tests against fixture mailbox. | +| 3. WhatsApp ingestion + send | 2 | Cloud adapter (primary, already verified) + Periskope adapter (fallback) both implemented behind shared interface. Webhook signature verification tested. RFQ + nudge send delivers. Inbound capture + extraction enqueue working. | +| 4. Extraction + agents | 2 | Demo extractor deleted. All 4 agents run on 1-min cron platform with per-org jitter. Cost guard live. `agent_run.quality_signals` data capture wired. Agent tests against fixtures. | +| 5. Tier 1 feature-surface hardening | 1.5 | Pagination, read-state, alerts firing, **hardened onboarding wizard with empty-state library plumbed into every Tier 1 page**. Tier 2 pages get smoke-tested for no-regression only. | +| 6. Ops + final polish | 1.5 | Sentry + Better Stack live. Runbook complete. Cron-health page. `delete-org.ts` script. Schema migration policy documented + tested. Privacy policy page. Security review pass (token storage, webhook signatures, cross-tenant audit). | + +--- + +## 6. Decision log + +- **Audience**: 3–5 design partners, not public launch. +- **Self-serve Clerk sign-up**: yes; if random sign-ups become noisy during design-partner phase, add a lightweight invite-code gate (not in v1). +- **Voice notes out** for v1. +- **All 4 agents in** (vendor-discovery deferred). +- **Approach B** chosen: full prototype to prod, but split into **Tier 1 (production-grade) and Tier 2 (no-regression)**. +- **Isolation: separate repo** (Option 2). Strongest "demo never breaks" guarantee. Porting tax accepted. +- **Periskope as WhatsApp fallback** — kept warm, swappable via env flag. Meta Cloud is the primary path (already verified). +- **Gmail token scoping**: per-user. `key_version` column for rotation. `needs_reauth` UX for refresh-token invalidation. +- **Cron platform**: Vercel Cron 1-minute minimum. No 30-second loops. Cron jitter per-org via `hash(org_id) mod 30 min` to avoid Anthropic thundering herd. +- **Static `org_id` lint check** in CI from Phase 0. Runtime cross-tenant tests are necessary but not sufficient. +- **Schema migrations**: forward-compatible only, no `drizzle-kit push` in prod, Neon staging branch first. +- **Partners start clean**: no demo → prod data import. Demo data is for sales, not for partner workspaces. +- **Engineering**: Siddhant + Claude. Estimate baselined at 13 weeks. +- **Onboarding wizard** gets the same hardening rigor as ingestion (§4.5.1). +- **Agent quality signals**: `agent_run.quality_signals jsonb` data capture lands in Phase 4 even though the surfaced UI is v1.1. + +--- + +## 7. Open questions to resolve during execution + +These are not blockers for the plan but should be answered before the relevant phase starts: + +- **Domain**: `app.tradyon.ai` — confirm DNS access and SSL strategy with Vercel during Phase 0. +- **Clerk pricing tier**: free tier covers <10k MAU; we'll be well under. Confirm at Phase 0. +- **Anthropic spend cap**: project-level monthly cap. Suggest $500/month for the design phase; revisit after first partner is live. +- **Privacy policy hosting**: confirm Tradyon legal team has a template, or draft from scratch in Phase 6. +- **Onboarding teammate invites**: partners can invite teammates via Clerk's org-invite UI — confirm Clerk's free-tier supports org invites or budget for the paid tier.