Scope every tenant-owned operation
Maya can read Acme's tasks. She discovers a task identifier belonging to Orbit and substitutes it in an API request.
UUIDs make identifiers difficult to guess accidentally. They do not establish permission. Identifiers can appear in logs, browser history, links, exports, and legitimate shared messages.
The API must reject access even when the identifier is valid and known.
Authorization and selection work together
First, the server verifies Maya's membership in the workspace named by the request. Then, the task lookup requires both the task identifier and that workspace identifier.
// Illustrative lookup after membership verification.
const task = await db.select().from(tasks).where(and(
eq(tasks.workspaceId, scope.workspaceId),
eq(tasks.id, requestedTaskId),
));
If the supplied task belongs to another workspace, the query returns no row. The route does not load that other tenant's record and decide what to hide afterward.
The same scope must apply to updates and deletes. A scoped read followed by an unscoped write creates another opportunity for mistakes. A single operation should carry its scope throughout its work.
Parent relationships need checks too
A task creation request supplies a project. The project must belong to the authorized workspace. Composite foreign keys additionally prevent storing crossed workspace relationships if application code makes a mistake.
Those constraints protect relational consistency. They do not restrict ordinary reads to a caller's tenant, so application queries still need predicates.
Row-level security, or RLS, is a PostgreSQL feature that can enforce row access policies within the database. It can add another layer, but it requires careful connection role and session-context design. Connection pools complicate that context. Taskboard's core enforcement uses application scope and relational constraints; this lesson does not claim RLS is enabled.
A common failure is remembering tenant conditions on task lists but forgetting them on comments, exports, activity feeds, or background jobs. Every path that touches tenant-owned data needs the same reasoning.
Does the database's task-to-project foreign key prevent a cross-tenant read?
No. It prevents an inconsistent relationship being stored. The read still needs an authorized workspace scope and a workspace restriction.
Membership establishes access to a workspace. Scoped queries establish which records the operation can touch.
Read api/src/tenancy.ts Read api/src/tasks.ts