Skip to Content

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_keys available 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 update bucket, which only patches rows already present — the selection silently disappeared and no foreign key was written on save. Picks are now staged in a dedicated link bucket 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) whenever fields was null, 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: activeTab defaulted to a fields tab 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.

Last updated on