The trust story your compliance lead reads in one sitting.
One short page for the finance, healthcare, government, legal, insurance and education buyers we now onboard. Where customer data lives. What gets logged and how long it is kept. Who can read it. And a one-line way to request the DPA, a privacy review, or signed terms — without a Slack thread.
Where Microsoft Graph refresh tokens and the issues_history table live
Two artefacts carry customer PII on the platform — the cached Microsoft Graph refresh token, and the issues_history table where the scan walker records every finding it surfaces. Each lives in a different layer, with a different residency story.
Microsoft Graph refresh-token cache
- The cached refresh token is per-admin and scoped to the signed-in admin's own Entra tenant (via the offline_access scope). Token issuance and rotation both happen inside that tenant — Wardwell never holds a multi-tenant application token.
- Rotation is driven by Microsoft's token endpoint, not by Wardwell's own cron. The cache is revoked via RFC 7009 at the start of the cancel flow; the local row is nulled inside the same transaction that tombstones the tenant, so the durable kill switch is the database row, not the Microsoft response.
- The cached tokens sit alongside better-auth's session table on the same managed Postgres instance that hosts the scan rows — so data residency inherits the managed region declared by the platform's rdbms capability in polsia.toml.
issues_history · the rolling history table
- The ScanFinding rolling-history table that backs the /issues page lives on the same managed Postgres instance, and inherits its managed region. Wardwell does not co-locate a database anywhere else.
- There is one sub-processor for the database itself: the platform's rdbms capability declared in polsia.toml. No replication to a second region, no off-cluster export.
- The data subject for GDPR purposes is the tenant admin who registered the app. No end-user personal contact data (no People API reads, no contact or calendar pulls, no Teams message inspection) is ever collected, so the per-finding rows do not carry end-user PII.
What audit logging captures on every scan
The platform is the source of truth for audit events. Each scan logs the same three artefacts — its wall-clock window, the per-issue list, and the per-finding history — and the better-auth session lifecycle stamps the surrounding sign-in events. Anyone answering an auditor can read all of it from one screen.
Scan -> startedAt, completedAt, errorMessage
ScanIssue -> per-issue detail (one row per issue surfaced)
ScanFinding -> observedAt, lastSeenAt (rolling history)- A Scan row records startedAt, completedAt, and any errorMessage — so an auditor can size the scan window without reading log output.
- Each per-issue ScanIssue row carries the issue payload — broken-link candidate, redirect-chain picture, observed URL — so the trail from startedAt to a specific finding is queryable per scan run.
- Per-finding ScanFinding rows carry observedAt and lastSeenAt, so an auditor following one broken-link case can confirm when it was first seen and when the acknowledgement stamp landed.
- Better-auth stamps every sign-in, sign-out, and refresh — including the issuing IP and user agent — so the admin can see who was in the app at any moment when an /api/issues read returned.
Who can read those records
Every audit event — sign-in, sign-out, session refresh, scan run, case opened, finding acknowledged — surfaces inside the app rather than in a separate console. There is no second tool to provision, no separate grant of access, and no Slack thread to scroll.
- Tenant admins read the audit surface inside the app on /issues — the same view they triage findings in. No PII payload, no separate log-shipper console.
- Page-level access patterns (which /scan or /issues routes loaded, traversal order) come from polsia-analytics — without PII.
- The Polsia ops team reads fewer of these rows for ops than a tenant admin reads for triage. Nothing outside the platform reads tenant scan data.
- Sign-in forensicsship in the same surface: the admin's profile lists the better-auth session with issuance and revocation timestamps, so "who was signed in at 14:03 last Tuesday" is a single page away.
One surface, one audit answer
Retention windows
Three retention windows cover the audit surface — the rolling history on ScanFinding, the tenant-lifetime rows on Scan / ScanIssue, and the out-of-band digest email that bridges the rolling window.
30-day rolling window on ScanFinding
What this is — and is not
- The 30-day window covers the per-finding rolling history only — ScanFinding rows. The parent Scan and the per-scan ScanIssue rows stay for as long as your tenant is on board; they are not part of the rolling window.
- A weekly digest email (jobs/weekly-issues-digest.js) ships the per-tenant ScanFinding summary to the tenant admin inbox and is the canonical out-of-band audit artefact — even after the rolling window trims the table, the digest remains in the inbox.
- Account deletion is a single transaction (with one best-effort pre-leg): the tenant row is tombstoned; microsoftRefreshToken is nulled; every ScanFinding row belonging to the tenant admin is anonymized in-place (deterministic salted hash on siteUrl, url, detail — values recoverable only within one deploy, see src/lib/business/anonymize.ts:hashField); a TenantAuditLog row stamps action='cancel', outcome='ok' or outcome='partial_revoke_failed'. The Scan and ScanIssuerows stay — the audit trail of what we observed at what wall-clock time is preserved on the cancelled tenant's tenantId. The RFC 7009 Graph revoke runs first as a best-effort leg (5s timeout, outcome string best_effort_failed if non-2xx).
- The cron schedule is declared versioned in polsia.toml alongside the schema, so any change to the window is a reviewable diff against the deployment record.
Sub-processors
Three names appear on the data-flow diagram, in this order: Polsia, Stripe, and Microsoft. Only the first two are Polsia-side sub-processors receiving customer scan data — Microsoft operates the identity layer on the customer's own Entra tenant.
Sub-processor list · what leaves Wardwell
- Polsia— operates the platform, including the managed Postgres instance declared by the platform's rdbms capability in polsia.toml. The Scan, ScanIssue, ScanFinding, Tenant, TenantAuditLog, DeletionReceipt, and better-auth session rows all live on the same managed Postgres instance.
- Stripe — processes payments on /pricing. The stripe-billing module documents this in detail (see /pricing). Tenant scan data does not flow through Stripe.
- Microsoft— the operator of Entra ID and Microsoft Graph. The data subject for the cached refresh token is the tenant admin's own Entra tenant; Wardwell never holds a multi-tenant application token, and the scan walker reads SharePoint file metadata only (path, etag, last-modified stamp, redirect-chain picture) — never raw file bodies.
Strict-DPA framing
Request the DPA, a privacy review, or signed terms
Request the DPA, a privacy review, or signed terms. Drop your work email and a one-line note about the request (DPA, security review, procurement schedule) and we'll come back within one business day. NDA on request.
We use this address only to schedule the compliance review. No marketing list, no re-marketing.
Or skip the legal review
See plans & start a trial.
Going live is a one-tenant pilot on managed Postgres, with a five-minute Entra app-registration flow and a per-tenant demo walkthrough on us.