Workflow
Auditor Access
Grant external auditors time-limited, scoped access to your compliance records. A single magic-link flow issues an HMAC-signed session cookie for every access level — no Supabase signup required. Auditors and org members share a bidirectional comment thread per entity with optional evidence attachments. Every action logged append-only for DORA Art. 5 and ISO 27001 A.5.28.
Access levels
3
Read-only · Comment · Full
Scopes
6
Evidence · Controls · Policies · Incidents · Risks · SOA
Default expiry
90 days
Auto-revokes; configurable per grant
Session TTL
8 hours
Re-reads DB every request — revokes are immediate
Who can grant access
Only users with the OWNER or ADMIN role can grant, edit, or revoke auditor access. The endpoint is POST /api/auditors and is RBAC-gated. Lower roles see the Auditors page in read-only mode via the normal PermissionGate.
How to grant access — step by step
- Go to Dashboard → Auditor Collaboration and click Grant Access.
- Enter the auditor's email. The invite is personal to this address; a different email signing up later is rejected.
- Pick the audit firm (used only as a label in the UI and email).
- Choose the access level:
- Read-only — the auditor receives a magic-link invite, no signup required. Best for short engagements.
- Comment — auditor can also leave review comments on evidence. Requires signup so comments attribute to a real identity.
- Full — auditor can view, comment, and download. Also requires signup.
- Check the scopes the auditor will see. Defaults to Evidence only. Adding more scopes broadens what the auditor can review — keep least-privilege in mind (DORA Art. 28).
- Optionally narrow vendor scope if evidence or risks are in scope. Empty = all vendors.
- Set an expiry date or leave blank for 90 days. Access is auto-revoked at midnight on the expiry date by the daily
auditor-access-expirycron job. - Click Send Invite. An invite email is sent via Resend; a warning toast appears if the email could not be delivered, with a one-click Resend.
Invite tokens are never stored
AuditorAccess.tokenHash. A leaked database dump cannot be used to impersonate an auditor — the attacker would need the raw token from the original email.Access levels explained
All three access levels use the same cookie-only flow — the auditor clicks the invite link and lands directly in the portal. An HMAC-signed session cookie (8 hour TTL) is issued and no Supabase user row is created. Attribution lives on the AuditorAccess row itself plus snapshotted email + firm fields on every record the auditor writes, so the audit trail survives revocation.
- Read-only — view only. No comment composer, no download buttons. Best for certification-body spot checks.
- Comment — everything read-only sees, plus the ability to post review notes on any in-scope entity and attach existing evidence items as context for an org reply.
- Full — comment-level permissions plus signed-URL download of evidence files. Use this for deep engagements (external ISO 27001 certification, DORA TLPT audit prep).
An auditor's level can be changed after the fact via PATCH /api/auditors/[id]. Capability changes take effect on the next request — resolveAuditorSession re-reads the row from the DB every time; the session cookie only proves identity, never capabilities.
Accepting the invitation
The auditor clicks the link in the email. The landing page at /auditor/accept exchanges the token for an HMAC-signed session cookie and redirects to /auditor/portal. No signup, no Supabase round-trip, no password — the cookie itself proves identity for the 8-hour window, and every portal request re-reads AuditorAccess for authorisation.
The accept endpoint:
- Rate-limits every POST to 10/min per (IP × token prefix) so a brute-force attempt on an unknown token cannot scan the space.
- Consolidates every failure (bad token, revoked, expired) into a single 404 so we don't leak the existence of a token.
- Logs
ACCEPT_INVITE(first visit) orREACCESS_INVITEtoAuditLogwith the auditor's email and client IP.
What auditors see in the portal
The portal at /auditor/portalis a minimal, dashboard-free surface. The top-bar shows only the auditor's firm, email, and access level. The tab nav is rendered from the grant's scopes — an auditor never sees a tab they were not granted.
- Evidence — list of evidence items in scope (filtered by
vendorIds). Rows expand to show metadata, versions, and the review-notes thread. FULL auditors get a signed-URL Download button on rows with a realfileUrl; metadata-only rows display No evidence attached. - Vendors — scoped vendor list with risk tier and active-contract flags.
- Controls & SOA — register with status and owner; clickable rows expand to the same shared detail dialog used in the dashboard, including review notes.
- Policies — published-version list with effective dates. Expansion shows the document body and the review-notes thread.
- Incidents — DORA Art. 19 register with severity, timeline, and deadline tracking. Expansion shows the details / timeline / actions / review-notes tabs.
- Risks — ISO 27001 Clause 6.1 register with treatment and owner. Expansion shows the thread.
Every API call in the portal goes through /api/auditor/* routes which share a common auth helper, resolveAuditorSession(). That helper:
- Verifies the HMAC signature on the session cookie.
- Reads the live
AuditorAccessrow from the database (never trusts the cookie's payload for permissions). - Rejects revoked, expired, or deactivated grants immediately.
- Bumps
lastAccessedAtso granters see when the auditor was last in. - Returns the current
scopesandvendorIdsfor the calling route to enforce.
Terms of Engagement (first portal visit)
The first time an auditor lands on /auditor/portalafter clicking their magic link, they're redirected to /auditor/toe— a blocking Terms of Engagement page. The page covers what they're accessing, confidentiality expectations carried over from their engagement letter with the granting organisation, audit-log semantics, and what they must not do (share cookies, exceed scope, copy evidence out-of-engagement). They check a single acknowledgement box and click Accept.
Acceptance sets AuditorAccess.auditorTermsAcceptedAt to NOW() and writes an ACCEPT_INVITEentry to the granting org's AuditLog with the IP address and user-agent captured from the request. Returning visits skip the TOE — the portal layout only redirects when the column is still null. If the granter ever clears the column, the auditor re-accepts on next visit.
Regulated customers (CSSF / DORA) specifically ask for documented acknowledgement of confidentiality and audit-log expectationsbefore external access is granted. The TOE + AuditLog row is that documentation.
Bidirectional collaboration
The portal and the dashboard share a single polymorphic AuditorComment table. Auditors and org members both post into it; both sides see the same thread woven into chronological order; and every reply produces a real-time notification on the other side. Threads are append-only — no edits, no deletes — so the audit trail is safe for ISO 27001 A.5.28 retention.
A comment is uniquely identified by (entityType, entityId) —Evidence, Control, Policy, Incident, or Risk. The authorType discriminator (AUDITOR vs ORG_MEMBER) drives the visual styling (amber vs blue accent) and determines which snapshot fields are populated on the row.
Auditor → org notification
- On
POST /api/auditor/comments,notifyUserdispatches an in-app bell badge + an email to the original granter and everyCOMPLIANCE_OFFICERon the org. - The notification link includes
?focusId=<entityId>. Each dashboard register page (controls, policies, incidents, evidence, risks) reads this param, expands the matching row, and scrolls it into view so the reviewer lands exactly on the threaded entity. - The bell polls every 15 s and refetches instantly on
visibilitychange+window.focusso new notes appear within ~1 s of tab wake.
Org → auditor reply
- Any role that can read the entity can reply via
POST /api/entity-comments. Permission matches whatever the central RBAC matrix defines forread:<entity>. - Every reply fans out an email via
sendAuditorReplyEmailto every distinct auditor who has commented on the thread, filtered to grants that are still active, unrevoked, and unexpired. - Auditors receive no in-app bell (no
Userrow), so email is the canonical channel.
Evidence attachments
- Both sides can attach existing
Evidencerows to any reply via a sharedEvidencePicker(search + multi-select, max 10 per comment). Attachments are linked through theAuditorCommentEvidencejoin table and are append-only — you can't un-attach after sending. - Rendered attachment chips link to a signed URL. Org side hits
GET /api/evidence/download?id=; auditor side (FULL only) hitsGET /api/auditor/evidence/[id]/download. - Vendor-scope bypass. A vendor-scoped auditor can download evidence that falls outside their normal
vendorIdsrestriction when an org member has intentionally attached it to a thread the auditor has scope for. Still gated on orgId, soft-delete, and FULL access level. - Metadata-only evidence (no
fileUrl) renders as a greyed-out chip that can't be clicked — no signed-URL dialog is ever generated for an empty record. - Notifications & audit-log previews suffix
[N evidence items attached]so reviewers know what to expect before clicking through.
Security features
- Hash-at-rest tokens. Raw invite tokens are generated with
crypto.randomBytes(32)(256 bits of entropy), delivered in the email once, and never persisted. The database holds onlySHA-256(token)intokenHash. - Stateless session verification. The session cookie is an HMAC-signed compact token covering
{accessId, orgId, iat, exp}. Signed withAUDITOR_JWT_SECRET(separate from any other platform secret). No DB round-trip to verify identity, but every request still re-reads the access row for authorisation. - HttpOnly + SameSite=Lax cookie. Not accessible to JavaScript. Marked
Securein production. - Email-identity hard match. For Comment/Full auditors, the Supabase signup email must equal the invited email (case-insensitive). A mismatch signs the user out and rejects the link-up — no silent fallback.
- Rate-limited. Accept endpoint is 10/min per (IP × token); resend is 3/hour per grant. Both return
429withRetry-After. - Append-only audit trail.
CREATE,UPDATE,REVOKE,ACCEPT_INVITE,VIEW,DOWNLOAD,COMMENT,EXPIRE, andAUTH_FAILUREall write toAuditLogvialogAudit(). Auditor entries havesource = "auditor"so you can filter the log per external party. - Immediate revocation. Clicking Revoke sets
isActive = false,revokedAt, andrevokedBy. The next request the auditor makes fails atresolveAuditorSession(), not at cookie expiry. Defence-in-depth beyond the 8 hour session TTL. - Auto-expiry cron. A daily job
auditor-access-expiryrevokes any grant whoseexpiresAthas passed, writes anEXPIREaudit entry, and emails the granter with a one-click re-grant link. Runs viaGET /api/cron?job=auditor-access-expirywith aCRON_SECRETbearer.
Managing granted access
- Lifecycle states shown in the table:
- Pending — invite sent, auditor hasn't accepted yet.
- Active — accepted, not expired, not revoked.
- Expired — past
expiresAt; auto-revoked by cron. - Revoked — manually revoked by an admin/owner.
- Resend. For Pending grants, click Resendto rotate the token and re-send the invite email. The previous email's link is immediately invalidated. Rate limit: 3/hour.
- Revoke. Click Revoke on Active or Pending grants. The access row is deactivated and all active sessions are terminated on the next request.
- Edit scope or level.
PATCH /api/auditors/[id]allows editing accessLevel, scopes, expiresAt, or vendorIds without a revoke-then-re-grant cycle. Currently exposed via the API; UI button to be added in a follow-up. - Last accessed.The table column shows when the auditor last hit any portal route. A blank "Never" on a Pending grant + several days elapsed is a good prompt to Resend or follow up.
Regulatory mapping
- DORA Art. 5 (Governance) — external-party access is logged append-only, attributable to the granter and to the auditor's session.
- DORA Art. 28 (Third-party risk) — per-grant scoping + vendor filtering lets you enforce least-privilege when granting access to auditors of ICT third-party service providers.
- ISO 27001 A.5.18 (Access rights) — access is time-limited with auto-expiry, and explicit revocation is recorded.
- ISO 27001 A.5.28 (Supplier & external-party access) — every VIEW / DOWNLOAD / COMMENT is captured in AuditLog with
source = "auditor". - ISO 27001 A.8.5 (Secure authentication) — hash-at-rest tokens, HMAC session signatures, Secure/HttpOnly cookies, email-identity binding.
Deferred to a follow-up release
- NDA / Terms-of-Engagement gate on first portal visit — some regulated customers want a signed-and-timestamped NDA capture before any data is shown.
- Watermarked PDF downloads — dynamic overlays with auditor email + access timestamp on every downloaded evidence PDF.
- Threaded @mentions of specific org members so a reply can route to one person instead of broadcasting to all Compliance Officers.
- Per-comment resolution state ("mark as addressed") so long engagements don't leave threads ambiguous.
- Digest emails on hot threads — today every reply is its own email; a simple 15-min digest would blunt notify-spam without losing signal.
- UI for access-level / scope edits — the
PATCH /api/auditors/[id]endpoint supports them but the Auditors page currently only exposes grant + revoke.
Troubleshooting
- "Invite email did not arrive." Check the toast after Grant Access — if the API returned
emailSent: false, the Resend call failed. Common causes:RESEND_API_KEYunset (logs[email] skipped), sender domain not verified in Resend, or the recipient domain bouncing. Use the Resend action on the toast (or the button on the Pending row) to retry. - "Invalid or expired link."The 404 response is intentional — we don't tell the client which of token unknown / already revoked / past expiry / already accepted elsewhere triggered it. Look in the server logs or re-issue via Resend.
- Portal tabs empty. Check the grant's
scopes. An empty scopes array defaults to Evidence only; non-Evidence tabs are hidden by the portal layout.