## Summary
Adds an external **Partner** self-service role that sees and edits only
its own
records via row-level security (RLS), so a validated partner can sign in
and manage
just the deals they're matched on.
## What's included
- **`partnerUser` relation** on Partner, Person, Company, Opportunity (+
inverse
relations on Workspace Member) — the login member a record belongs to.
- **RLS predicates** scoping each of those objects to "partnerUser IS
the current
workspace member", plus a self-scope on Workspace Member so member-typed
relations
resolve without exposing the internal team roster. Applied out-of-band
via
`yarn rls:configure` (the app manifest cannot ship RLS predicates).
- **Assign / unassign cascade** (`on-opportunity-partner-assigned` logic
function):
assigning a Partner to an Opportunity stamps `partnerUser` onto the
Opportunity +
its Company + People; removing the Partner clears it (and cascades to
the
Company/People when no other deal of that member still uses them).
- **Partner role permissions**
- Partner profile: full read/update.
- Opportunity: read all; **update `stage` and `amount` only** (every
other
user-facing field locked).
- Company / Person: read-only.
- Workspace Member: read-only, RLS-scoped to self.
- **`partnerUser` column** added to the Validated Partners view so the
login member
can be assigned inline.
## Install / upgrade note
After install or reinstall, run `yarn rls:configure` (`:prod` variant
for prod) to
(re)apply the RLS predicates and verify the field-permission locks.
Manifest sync
handles object/field permissions; predicates are applied by this script.
## Platform gaps found (for the eng team)
1. **Manifest sync doesn't invalidate the roles-permissions Redis
cache.** Permission
changes deployed via `yarn twenty dev --once` persist to the DB but
aren't reflected
in the cached snapshot used for enforcement until
`engine:workspace:metadata:permissions:roles-permissions:<workspaceId>:{data,hash}`
is flushed. Relevant on any real workspace when permissions change.
2. **Locking a server-injected field silently breaks all updates.** The
`*.updateOne`
pre-query hook writes `updatedBy` into every update, so
`canUpdateFieldValue:false`
on `updatedBy` makes the permission check reject *every* record update
with
`PERMISSION_DENIED`. Field-permission lock lists must exclude
server-managed/injected
fields (`updatedBy`; and `position`, co-written with `stage` on kanban
drag).
## Version
Minor bump → `0.5.0` (new role, new fields, new behaviour;
backwards-compatible).
## Testing
- Verified locally as a partner user: edits own profile; edits
Opportunity stage +
amount; Company/Person read-only; sees only matched deals; unassigning a
partner
removes the deal (and its company/people) from the partner's view.
- `yarn rls:configure` passes (5 predicates upserted; 24 Opportunity
fields locked,
stage + amount editable).
- Lint clean.
twenty-partners
A Twenty app that turns the CRM into the operating system for the Twenty partner program: intake partner-eligible deals, match them to vetted marketplace partners, and track the matching pipeline end-to-end.
Built on Twenty with twenty-sdk v2.5.
What's inside
- Custom object:
Partner— slug, status, availability, served geos, languages spoken, deployment expertise, Calendly link, last-match timestamp. Seesrc/objects/partner.object.ts. - Opportunity extensions —
matchStatus,designDocStatus,introSentAt,lastRelanceSentAt,tftId, plus apartnerrelation. - Logic functions
on-opportunity-auto-match— fires whenmatchStatusis set toAUTO_MATCH. Assigns the longest-idle available partner and flips status toMATCHED. If no partner is available, hands off toMANUAL_MATCHwith an audit Note explaining why.list-available-partners— surfaces matchable partners for a given opportunity.post-install— first-run setup.
- Roles (
src/roles/)- Twenty Partner Ops — internal team role, full CRUD on Partner/Company/Person/Opportunity.
- Partner — placeholder external-partner role. Do not assign until Twenty ships row-level permissions — it currently grants access to every record.
- Views (
src/views/)Waiting for match— opportunities awaiting human action (matchStatusisTO_BE_MATCHEDorMANUAL_MATCH).Matches overview— full matching funnel grouped bymatchStatus(configure Kanban grouping manually in the UI).Opportunities— replacement of the native opportunities view with the partner columns.PartnersandAll matched deals— partner-side index and deal log.
- Sidebar nav — surfaced in workflow order:
Waiting for match,All partner deals,Matches overview,Partners,Opportunities. - Seed scripts (
src/scripts/) — populate a fresh workspace with realistic demo data.
Match status pipeline
matchStatus is a non-nullable SELECT field with a default of TO_BE_MATCHED. The 10 states follow the deal lifecycle:
| Status | Meaning |
|---|---|
TO_BE_MATCHED |
Default — deal entered, awaiting assignment |
MANUAL_MATCH |
Needs a human to pick a partner |
AUTO_MATCH |
Triggers automatic partner assignment |
MATCHED |
Partner assigned |
INTRODUCED_TO_A_PARTNER |
Customer intro sent |
WORKING_WITH_A_PARTNER |
Engagement underway |
IMPLEMENTING |
Active implementation |
WON |
Deal closed won |
RECONNECT_LATER |
Paused — reconnect in future |
LOST |
Deal closed lost |
Getting started
Requires a local Twenty server at http://localhost:2020 and Node ^24.5.
yarn install
yarn twenty dev
Default dev credentials: tim@apple.dev / tim@apple.dev.
Run yarn twenty help for the full CLI reference.
Common commands
| Command | What it does |
|---|---|
yarn twenty dev |
Start the dev server and sync the app on file changes |
yarn twenty server status |
Check the local Twenty server |
yarn lint / yarn lint:fix |
Run oxlint |
yarn test |
Run integration tests (vitest.config.ts) |
Seeding demo data
Two idempotent seed scripts. Both run via the vitest.seed.config.ts config that skips
the global app uninstall/reinstall.
# 1. Marketplace partners (run first — pipeline seed wires opportunities to these by slug)
yarn vitest run --config vitest.seed.config.ts src/scripts/seed-marketplace-partners.ts
# 2. Pipeline demo: 3 companies, 3 people, 15 opportunities spread across matchStatus values
yarn vitest run --config vitest.seed.config.ts src/scripts/seed-pipeline-demo.ts
Both scripts skip records that already exist (by slug, name, or firstName+lastName),
so they are safe to re-run.
Known limitations
Current SDK gaps blocking further polish:
- Custom Partner record page layout (RECORD_TABLE has no relation scoping).
- Native Opportunities view column-order override.
- Kanban view configuration from app code (
ViewType.KANBANis currently ignored). - App and field descriptions.