Permissions
Hule has a layered access control model. Each layer is checked in order, and every layer must pass.
Layer 1: Authentication
Every request (except auth endpoints) requires a valid JWT access token. The token identifies the user and carries their profile information. Public endpoints — login, registration, password reset — are exempt.
Layer 2: Rate limiting
Requests are rate-limited by IP address. Auth endpoints (/auth/*) have stricter limits to prevent brute-force attacks. If you exceed the limit, you'll receive a 429 Too Many Requests response.
Layer 3: Workspace membership
To access any workspace-scoped resource, you must be an active member of that workspace. Membership is checked live on every request — caching is not used, so changes (removal, leaving) take effect immediately.
Workspace membership comes in two states:
| State | Meaning |
|---|---|
| Active | Full member — can see and be granted access to content |
| Pending | Invite sent but not yet accepted — no access to workspace content |
Workspace membership alone does not grant access to spaces, lists, or views inside the workspace. It's a prerequisite check, not an authorisation.
Roles
Every active member has a role. The owner is authoritative via Workspace.ownerId (no membership row needed); Admin/Member come from the membership row.
| Role | Capabilities |
|---|---|
| Owner | Full control. Can change roles, delete the workspace. Has implicit edit access to every entity (no Share row required). |
| Admin | Can invite/remove plain members and grant/revoke Shares. Cannot manage the owner or other admins. No implicit entity access — needs a Share (or shareEverything) like a member. |
| Member | No member-management or sharing. Sees only the entities they hold a Share on. |
A per-member shareEverything flag grants a member a blanket edit Share on every entity in the workspace (current and future). Toggling it off removes exactly those blanket grants.
Layer 4: Entity-level sharing (Shares)
Inside a workspace, access to spaces, lists, and views is controlled by Share rows — explicit permission records scoped to one user and one entity.
How shares are checked
When you access a space, list, or view, the system looks for a Share row matching (entityType, entityId, yourUserId). If none exists, access is denied — even if you're a workspace member.
Two exceptions bypass the Share-row lookup: the workspace owner has implicit edit on every entity, and workspace-root views are readable by any active member without a Share row.
Access levels
| Level | What you can do |
|---|---|
| Read | View the entity and its contents. May also add comments (and the @mentions they carry) — comment creation deliberately requires only Read, not Edit. No other modifications. |
| Edit | Create, update, and delete tasks and comments within the entity. |
Cascade on share
When a space is shared, all its existing lists and views are shared at the same level. New lists or views created after the share are not auto-shared — the owner must grant access explicitly, or use the auto-grant flow in the share dialog.
Tasks and comments
Tasks and comments don't have their own Share rows. Access is derived:
- Task: check Share on the task's parent list.
- Comment: check Share on the comment's parent task → its parent list.
This means if you have Edit access to a list, you can create and edit tasks and comments inside it without any additional permissions.
Layer 5: Role-gated operations
Certain operations require Admin or Owner:
- Inviting and removing members (an admin may remove plain members only, never the owner or another admin).
- Granting and revoking Shares (an admin may manage plain members only).
- Configuration: creating/updating folders, lists, views, templates.
A few operations remain owner-only:
- Changing a member's role or
shareEverythingflag. - Deleting the workspace.
Self-service exceptions: any grantee may self-revoke (leave) a Share granted to them, and any member may leave the workspace.
These are enforced by @RequireRole guards and re-checked at the service layer. An unauthorised attempt gets a 403 Forbidden.
Summary
Request
│
▼
JWT Auth Guard ← Layer 1: valid token required
│
▼
Rate Limiter ← Layer 2: per-IP limits
│
▼
Member Guard ← Layer 3: active workspace member?
│
▼
Share Guard ← Layer 4: Share row exists? (owner + root-views bypass)
│
▼
Role Guard + service ← Layer 5: role-gated (admin/owner) constraintsRelated
- Workspaces — membership management.
- Sharing — how to share spaces, lists, and views.