Skip to content

feat(retention): delete an organization from Settings - #473

Merged
MartinPTielemans merged 2 commits into
mainfrom
feat/qui-252-delete-organization
Oct 24, 2026
Merged

MartinPTielemans merged 2 commits into
mainfrom
feat/qui-252-delete-organization

Conversation

@MartinPTielemans

@MartinPTielemans MartinPTielemans commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Nothing in the app can delete an organization. Better Auth's delete is switched off because it would remove only some rows, and the purge from #471 and #472 is not reachable from the UI. Customers need a way to have their data deleted, and a distribution needs to decide what that means for it, for example a waiting period before the purge.

Change

  • Settings → Company: "Delete organization" (Danish: "Slet organisation"). Only admins see it, and only when the runtime capability organizationDeletion.enabled is on. The copy says the deletion is permanent, reminds the user to keep invoices and credit notes for 5 years, and links to the existing export (Activity → Accounting). A dialog asks the user to type the organization's name.
  • settings.deleteOrganization mutation. It checks again, on the server, that the caller is a signed-in admin (agent keys are refused) and that the typed name matches. Then it calls requestOrganizationDeletion. When the organization is gone, it returns the user's next organization. The client switches to that organization, or signs the user out if there is none (a sole member's account goes with the organization).
  • RuntimeExtension.requestOrganizationDeletion(request), a new extension point. The first extension that sets it decides what the action does and returns { status: "deleted" } or { status: "scheduled", deleteAfter }. Without one (self-host), the organization is purged at once through purgeOrganization. A scheduled deletion must still end in purgeOrganization. A hosted distribution can use this to schedule a cancellable deletion after a waiting period.
  • Capability organizationDeletion: { enabled } in @quits/contracts/runtime. It defaults to on for self-host and off when QUITS_DISTRIBUTION=cloud, so a hosted upgrade does not show an instant purge before the hosted side opts in through resolveCapabilities. Consumers of older releases read it as off.
  • Better Auth's delete stays closed. disableOrganizationDeletion: true remains, and its comment now says why: every deletion goes through requestOrganizationDeletion and ends in the purge, so Better Auth's partial delete can't be used to get around it.

Read-only state. OSS already has the read_only organization access state (OrganizationAccess in lib/billing/types.ts); only document creation checks it. A new test puts an organization in read_only and shows that a private PDF download, a public PDF link, the public payment page and the accounting CSV export all still work.

Linear

Refs QUI-252

Verification

  • Integration (own Postgres, CI's synthetic auth env), all passing:
    • settings-delete-organization.integration.test.ts (new): members and accountants get FORBIDDEN; a mistyped name gets BAD_REQUEST; a distribution turning off the capability gets FORBIDDEN; with no extension, the organization is purged and the admin is sent to their other organization; an extension's scheduled outcome replaces the purge.
    • pdf-access.integration.test.ts (extended): the read-only case above.
    • organization-purge.integration.test.ts: unchanged, still passing.
  • Unit tests: runtime extensions and bootstrap, the Settings page tests (tax relevance, locale refresh, performance), Settings Stripe and VAT routers, onboarding capability discovery, the i18n catalog, and the contracts runtime tests.
  • vp lint --deny-warnings on the changed files; bun run typecheck:app; contracts typecheck.
  • Not done: no browser check and no screenshot of the new card. CI's browser suite covers Settings.

Risk

  • Ines design sign-off recorded in Linear, or no design change. The new card follows the existing Settings card and AlertDialog patterns; design review is still welcome.
  • Mira copy check completed against COPY.md and docs/public-promises.md, or no copy change. New da/en copy needs a check.
  • Noah production check recorded before Done, or pending under Needs Martin. Self-host only until a hosted release opts in.

On self-host, a deletion is permanent at once and can't be undone. That is intended, and it sits behind typed confirmation and the admin role.

Admins can delete the organization from the Company page of Settings
after typing its name. The server checks the role and the name again.
A new runtime extension point, requestOrganizationDeletion, decides
what the action does; without one the organization is purged at once.
The capability is on for self-host and off in the hosted distribution
until it opts in. Better Auth's own delete stays closed, so every
deletion goes through the purge.

Also proves PDFs, public links and exports keep working while the
organization is read-only.

Refs QUI-252
@linear-code

linear-code Bot commented Oct 24, 2026

Copy link
Copy Markdown

QUI-252

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-24T13:13:47.657820Z
We couldn’t complete this review. Comment @codex review to try again.
c9e0164 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

The card shows its own error, so the shared banner must not report it again.

Refs QUI-252
@MartinPTielemans
MartinPTielemans merged commit 79db4b3 into main Oct 24, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant