Relations (M2M)
RelationBuilder manages the entries in a many-to-many pivot table. It is accessed via pb.model(ModelClass, parentId).
// Access the relation builder for a specific recordconst room = pb.model(Room, roomId);
// room.<relationName>() returns a RelationBuilderawait room.members().attach(userId);The relation name (e.g. members) must match a property on the model decorated with @relation({ type: 'many-to-many', ... }).
@relation({ type: 'many-to-many', target: () => User, joinTable: 'room_members', // pivot table name foreignKey: 'room_id', // FK pointing to the parent (Room) inverseJoinColumn: 'user_id', // FK pointing to the related (User) pivotColumns: ['role'], // Extra columns to include from the pivot})members: User[];Methods
Section titled “Methods”.attach(ids, pivotData?)
Section titled “.attach(ids, pivotData?)”Add one or more records to the relation.
// Single IDawait room.members().attach(userId);
// With pivot dataawait room.members().attach(userId, { role: 'admin' });
// Multiple IDsawait room.members().attach([userId1, userId2, userId3]);
// Multiple with individual pivot dataawait room.members().attach({ [userId1]: { role: 'admin' }, [userId2]: { role: 'member' }, [userId3]: { role: 'member' },});Duplicate safe — if the relation already exists, the database ON CONFLICT DO NOTHING prevents errors.
.detach(ids?)
Section titled “.detach(ids?)”Remove one or more records from the relation. If no IDs are provided, removes all related records.
// Remove a specific memberawait room.members().detach(userId);
// Remove multiple membersawait room.members().detach([userId1, userId2]);
// Remove ALL membersawait room.members().detach();.sync(ids)
Section titled “.sync(ids)”Replace the current relation set. Members not in the new list are detached; those in the list but not attached yet are attached.
// The new exact set of members will be exactly [u1, u2, u3]await room.members().sync([userId1, userId2, userId3]);
// With pivot data per recordawait room.members().sync({ [userId1]: { role: 'admin' }, [userId2]: { role: 'member' },});Use sync when you have a definitive “current state” list (e.g., saving a form with a checkboxes).
.updatePivot(id, pivotData)
Section titled “.updatePivot(id, pivotData)”Update the extra columns on a specific pivot row without changing the relation itself.
// Promote a user to adminawait room.members().updatePivot(userId, { role: 'admin' });
// Update multiple fieldsawait room.members().updatePivot(userId, { role: 'moderator', joined_at: new Date().toISOString() });.toggle(ids)
Section titled “.toggle(ids)”Attach if not present; detach if already in the relation.
// Toggle a single userawait room.members().toggle(userId);
// Toggle multiple usersawait room.members().toggle([userId1, userId2]);
// Toggle with pivot data for new attachmentsawait room.members().toggle({ [userId1]: { role: 'member' },});Use toggle for like/follow/join buttons where a single action should alternate between attach and detach.
JWT-Bound Pivots
Section titled “JWT-Bound Pivots”When the inverseJoinColumn value matches jwt.sub (the current user’s ID), the relation is JWT-bound — the database automatically validates that users can only manage their own memberships via RLS.
// The current user joins a roomawait room.members().attach(pb.auth.getCachedUser()!.sub, { role: 'member' });
// The current user leaves a roomawait room.members().detach(pb.auth.getCachedUser()!.sub);Combined with an RLS policy using owner: 'user_id', this makes it impossible for any user to add or remove someone else via the API — enforced at the PostgreSQL level.