**Version:** twenty-partners `1.6.1` (`on-application-created` gains behaviour; no schema change). ## The bug On https://partners.twenty.com every partner could list **all** applications, not just their own. The Partner role's row-level predicate on `Application` was: ``` (partnerUser IS the current member) OR (lastActivityAt IS EMPTY) ``` The `IS EMPTY` branch was an insert escape hatch: the Apply workflow creates the row before `on-application-created` can stamp it, and RLS validates an insert against the row as submitted. Only that self-apply path ever writes `lastActivityAt` (`resolve-candidacy.service.ts`). Every other creation — admin invite, TFT import, seed — returned early, so `lastActivityAt` stayed `null` forever and the hatch never closed. All 7 applications in production had `lastActivityAt = null`, which made the whole table readable by every partner. ## The fix 1. **Drop the OR group.** `application` becomes a plain `partnerUser IS the current member` predicate, like the other partner-scoped objects. The upsert reconciles per (role, object), so the stale group and predicate are soft-deleted on re-run — a leaking workspace self-heals. 2. **Keep admin invites visible.** The OR branch was also the only reason an admin-created invite reached its recipient. `resolve-candidacy` now stamps `partnerUser` from the partner on the admin path, instead of returning early. 3. **Backfill the rows created before the narrowing.** Neither writer covers an application an admin created for an already-linked partner; those existed only behind the leak. `stampPartnerUserFromPartner` now covers `application` (its three copy-pasted branches collapsed into an accessor map routed through `shared/graphql/`), and the walk runs from the app's post-install logic function, gated on `previousVersion < 1.6.1`. No manual step. 4. **Preserve the insert path.** The Apply workflow must map exactly Opportunity + Partner User. `partnerUser` is writable at insert only because the server exempts RLS predicate fields there (`permissions.utils.ts`, insert case only); every other Application field is locked, so mapping `State` fails the insert — and `state` already defaults to `APPLIED`. ## Order of operations, per workspace 1. Publish the Apply workflow with the Partner User mapping (edit it if it already exists). 2. `yarn rls:configure` (`:prod`). `app:install` stamps the pre-existing rows before step 2 runs, so no window exists where a partner reads nothing. The script prints these steps before and after its writes, because the deploy path never opens the runbook. ## Verification (local bundle, real Partner-role account) | Case | Result | |---|---| | Another partner's application | not visible | | Own application (`lastActivityAt` null) | visible | | Admin invite created with `partnerUser` null | stamped from the partner within seconds | | All applications stripped of `partnerUser`, then upgraded from 1.6.0 | post-install returns `{ stamped: 3 }`; the 4th belongs to a partner with no member | | Upgrade from 1.6.1 | post-install returns `{ skipped: true }` | | Insert without `partnerUser` | rejected — *Record does not satisfy row-level security constraints of your current role* | | Insert with `partnerUser` = self | accepted | | Insert with `state` mapped | rejected — *no permission to write field "state"* | Lint 0, typecheck 0, 221 unit tests. ## Production notes - The fix lands on prod by running `yarn rls:configure:prod`. Installing this version alone does not narrow the predicate. - The 7 production applications were already backfilled by hand; the post-install hook makes that reproducible for any other workspace. ## Out of scope - Creating an OR predicate group fails on server 2.23.2 in a fresh workspace (`Migration action 'create' for 'rowLevelPermissionPredicateGroup' failed`). Pre-existing and unrelated; production is unaffected because its `opportunity` group already exists. It does block `rls:configure` on newly provisioned local bundles. - Partners still see the `Matching Admin Workspace` navigation folder. Navigation menu items cannot be scoped by role in the SDK; the views are row-filtered. - `configure-partner-rls.ts` should not exist. #21919 made `rowLevelPermissionPredicates` declarable on the role manifest, and the SDK we depend on already ships it, so the predicates belong in `partner.role.ts`. The predicates on the workspace today were written through the metadata API and are not app-owned, so adopting them needs its own migration and test pass. Follow-up. - Deferred cleanups are listed in the thermo review comments below.
The #1 Open-Source CRM
Website ·
Documentation ·
Roadmap ·
Discord ·
Figma
Why Twenty
Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves. Twenty is the CRM you build, ship, and version like the rest of your stack.
Learn more about why we built Twenty
Installation
Cloud
The fastest way to get started. Sign up at twenty.com and spin up a workspace in under a minute, with no infrastructure to manage and always up to date.
Build an app
Scaffold a new app with the Twenty CLI:
npx create-twenty-app my-app
Define objects, fields, and views as code:
import { defineObject, FieldType } from 'twenty-sdk/define';
export default defineObject({
nameSingular: 'deal',
namePlural: 'deals',
labelSingular: 'Deal',
labelPlural: 'Deals',
fields: [
{ name: 'name', label: 'Name', type: FieldType.TEXT },
{ name: 'amount', label: 'Amount', type: FieldType.CURRENCY },
{ name: 'closeDate', label: 'Close Date', type: FieldType.DATE_TIME },
],
});
Then ship it to your workspace:
npx twenty app:publish --private
See the app development guide for objects, views, agents, and logic functions.
Self-hosting
Run Twenty on your own infrastructure with Docker Compose, or contribute locally via the local setup guide.
Everything you need
Twenty gives you the building blocks of a modern CRM (objects, views, workflows, and agents) and lets you extend them as code. Here's a tour of what's in the box.
Want to go deeper? Read the User Guide for product walkthroughs, or the
Documentation for developer reference.
|
|
|
|
|
|
Stack
TypeScript
Nx
NestJS, with BullMQ,
PostgreSQL,
Redis
React, with Jotai, Linaria and Lingui
Thanks
Thanks to these amazing services that we use and recommend for code review (Greptile), catching bugs (Sentry) and translating (Crowdin).
Join the Community
Star the repo ·
Discord ·
Feature requests ·
Releases ·
X ·
LinkedIn ·
Crowdin ·
Contribute





