v0.1.90 — 10 Aug 2026
Feature — Module-Level Access: application capability flags in Policies
A new dimension of permissions beyond collection-level access. Module-level access
keys are named capability flags (e.g. system:logs, workflow:approve) that live
in a tree-structured registry and can be granted per-policy, independent of
collection CRUD permissions.
New daas_module_access_keys collection
The key registry stores capability identifiers in a parent/child tree. Folder nodes
(key = null) group related keys; only leaf nodes can be toggled on a policy.
- Key format:
^[a-z][a-z0-9_:./-]*$, globally unique - Managed at Module Access Keys in the sidebar (Access section, admin-only)
- MCP tool
module_access_keysavailable for AI-assisted management
System seeds: system:logs, workflow:approve, workflow:reject. Only keys the
application actually enforces are seeded — pages backed by a real collection
(activity, cron, extensions, services, settings) are delegated through ordinary
collection read permissions instead, so they get no parallel module key.
module_access column on daas_policies
Stores { key: boolean } flags. OR-merge semantics: a user gains a key if any
assigned policy sets it true. Admins bypass all checks.
Policy editor — new Module-Level Access tab
The Policy detail page now has two tabs:
- Record-Level Access — existing collection permission matrix
- Module-Level Access — grouped toggle tree of all registered keys
/api/permissions/me response extended
The response now includes moduleAccess: Record<string, boolean>. Access in
React components via usePermissions().hasModuleAccess(key).
Workflow commands support module_access_keys
Workflow command definitions may specify:
{ "module_access_keys": ["workflow:approve"] }The transition gate OR-merges policies and module_access_keys — satisfying
either allows the transition.
Existing workflows are fully backward compatible — module_access_keys is optional.
Logs page is delegable via system:logs
GET /api/logs and the SSE stream at /api/logs/stream accept the system:logs
key instead of requiring admin, and the sidebar Logs item appears for grantees.
Clearing logs stays admin-only — the key grants viewing, not destruction.
Unauthenticated requests to these endpoints now return 401 (previously 403).
Fixed — /api/permissions/me is now scope-aware
Who is affected: Multi-tenant deployments using Resource URI scoping.
What was wrong
The endpoint resolved policies with the scope-blind get_user_policies() while the
data layer resolved with get_user_policies_for_scope(). It was the only
scope-blind route in an otherwise scope-aware stack, so it advertised permissions
and module keys from sibling scopes that the API would then refuse — the UI
offered actions that 403’d on click.
What changed
Policy resolution now mirrors the data layer: an absent X-Resource-Uri /
daas_resource_uri keeps the flat behaviour exactly (backward compatible), an
empty value resolves at root scope, and a concrete URI resolves with upward
ancestor matching. Both data and moduleAccess derive from the same resolved
policy set, an invalid Resource URI returns 400 instead of 500, and the
response gained meta.resource_uri plus richer ?debug=true output.
Clients must key their permission caches on the active scope — a cache that ignores scope will serve the previous tenant’s permissions after a scope switch.
Fixed — workflow transition gates resolve at the caller’s asserted scope
Workflow transition policy and module-key gates previously resolved through the
flat get_user_policies(), so a policy or key granted at /tenant:1 also opened
the same transition at /tenant:2. Both gates now resolve at the scope the caller
asserts via X-Resource-Uri, and an invalid or unauthorized Resource URI on
POST /api/workflow/transition is rejected with 400/403 instead of being
silently treated as unscoped. Callers that send no scope at all keep the flat
behaviour for backward compatibility.
Added — last_access records every login as “Last Login”
Who is affected: Anyone reading daas_users.last_access — the Users list and
user detail page now label it Last Login.
A trigger on auth.sessions updates daas_users.last_access on session creation,
capturing every login source (DaaS API, direct Supabase client, OAuth callbacks)
regardless of entry point. Profile edits no longer overwrite the value — the
date-updated special was removed from the field — and existing users are
backfilled from their most recent session. This records the last login, not the
last request: an active user with a long-lived session keeps their timestamp until
they authenticate again.
The migration needs auth-schema privileges (supabase_auth_admin). On targets
without them it degrades to a warning and skips the trigger rather than failing
the deployment.
Fixed — list-o2m “Add Existing” defers to Save
Who is affected: Forms using the list-o2m interface. Component bumped to v1.10.0.
Two staging bugs around linking existing items:
- On an unsaved parent, a picked item was staged into the
updatebucket, which only patches rows already present — the selection silently disappeared and no foreign key was written on save. Picks are now staged in a dedicatedlinkbucket that renders immediately and emits the id + FK on save. - On a saved parent, picking an item linked it immediately via an API call,
bypassing
onChange— the form’s dirty-tracking never saw the change and Save stayed disabled even though the row was already linked server-side. Linking now always defers to Save.
Also fixes a mount-emit bug that could clear an existing O2M value on load.
Fixed — permission badges and modal for DELETE / SHARE
getPermissionLevel()returned “none” (gray) wheneverfieldswasnull, without checking whether a custom permissions/validation rule was saved. DELETE and SHARE never use fields, and UPDATE can carry an item filter without field restrictions — those badges now show custom when a rule exists.- The permissions modal opened blank for DELETE and SHARE:
activeTabdefaulted to afieldstab those actions don’t have. It now falls back to the first valid tab for the action.
Docs — workflow command policies reference daas_policies.id
New “Locking Commands to Roles” sections in the workflow docs spell out that
policies[] must reference daas_policies.id — not the daas_access junction
row id — with a step-by-step example and a warning about the silent 403 this
mistake produces. The WorkflowButton example was corrected to derive available
commands client-side from the workflow definition.