Summary
A folder or a project can't be shared with a specific person. The permission engine already supports it — only the API surface is missing:
doc_acl already models resource_type doc | folder | project × principal_type user | team | org_role × permission read | write | admin | none, with expires_at and a unique (resource_type, resource_id, principal_type, principal_id) grant.
app_effective_permission() already resolves precedence (explicit deny wins, then best positive across principals).
canAccess() (apps/api/src/lib/iam.ts) already implements inheritance: doc → folder → project.
But doc_acl rows are only ever written from routes/docs.ts and routes/org.ts, and routes/folders.ts has no sharing endpoints at all — nor does routes/projects.ts. So there is no way for an owner to add a collaborator to a folder or project and pick their access level.
Proposal
Add the missing CRUD, writing the same doc_acl rows the org access matrix already writes:
GET /api/{folders|projects}/:id/collaborators
POST /api/{folders|projects}/:id/collaborators
PATCH /api/{folders|projects}/:id/collaborators/:userId
DELETE /api/{folders|projects}/:id/collaborators/:userId
Because inheritance already exists, a grant on a folder automatically covers every doc inside it — no new resolution logic, no schema change.
- Managing shares requires
admin on the resource via canAccess() (which falls through to the workspace role, so owners/admins don't need an explicit grant on every resource).
- Unknown ids and ids from another tenant both
404 — existence is checked through RLS so this can't be used to probe for foreign ids.
- Every grant / update / revoke is written to
iam_audit_log.
Scope: the grantee must already be a workspace member. Inviting an outside email needs a pending grant that materialises on signup plus a restricted-visibility guest role — deliberately out of scope here, since that changes a security default and should be decided separately.
Question for maintainers (core vs enterprise)
CONTRIBUTING lists org-IAM-SSO as an enterprise module, but doc_acl, lib/iam.ts, routes/folders.ts and routes/projects.ts are all present in this repo — i.e. resource-level ACL looks like core, while org-wide IAM (roles/teams/SSO) is the enterprise part.
Is resource-level sharing core, or should it sit behind the enterprise seam? Opening this first per CONTRIBUTING ("if you're unsure which side a change belongs on, open an issue first"). Happy to relocate — the implementation is self-contained (one route file, one lib file, one registration line).
Acceptance criteria
- An owner/admin of a folder or project can list, grant, update and revoke a collaborator's access.
- Grants inherit to contained docs via the existing
canAccess() chain.
- Non-admins get
403; unknown/foreign resources 404.
- Actions are audit-logged; no schema change; core continues to build with enterprise modules absent.
Summary
A folder or a project can't be shared with a specific person. The permission engine already supports it — only the API surface is missing:
doc_aclalready modelsresource_typedoc | folder | project×principal_typeuser | team | org_role×permissionread | write | admin | none, withexpires_atand a unique(resource_type, resource_id, principal_type, principal_id)grant.app_effective_permission()already resolves precedence (explicit deny wins, then best positive across principals).canAccess()(apps/api/src/lib/iam.ts) already implements inheritance: doc → folder → project.But
doc_aclrows are only ever written fromroutes/docs.tsandroutes/org.ts, androutes/folders.tshas no sharing endpoints at all — nor doesroutes/projects.ts. So there is no way for an owner to add a collaborator to a folder or project and pick their access level.Proposal
Add the missing CRUD, writing the same
doc_aclrows the org access matrix already writes:Because inheritance already exists, a grant on a folder automatically covers every doc inside it — no new resolution logic, no schema change.
adminon the resource viacanAccess()(which falls through to the workspace role, so owners/admins don't need an explicit grant on every resource).404— existence is checked through RLS so this can't be used to probe for foreign ids.iam_audit_log.Scope: the grantee must already be a workspace member. Inviting an outside email needs a pending grant that materialises on signup plus a restricted-visibility guest role — deliberately out of scope here, since that changes a security default and should be decided separately.
Question for maintainers (core vs enterprise)
CONTRIBUTING lists org-IAM-SSO as an enterprise module, but
doc_acl,lib/iam.ts,routes/folders.tsandroutes/projects.tsare all present in this repo — i.e. resource-level ACL looks like core, while org-wide IAM (roles/teams/SSO) is the enterprise part.Is resource-level sharing core, or should it sit behind the enterprise seam? Opening this first per CONTRIBUTING ("if you're unsure which side a change belongs on, open an issue first"). Happy to relocate — the implementation is self-contained (one route file, one lib file, one registration line).
Acceptance criteria
canAccess()chain.403; unknown/foreign resources404.