Skip to main content
Private preview. fremforge is in private preview — invited customers only. Content is still subject to change. Request access →
Lifecycle

User and tenant lifecycle

This page covers what happens — and when — across the two lifecycles that matter most: the people you put on your org, and the org itself. Both are designed so that destruction is irreversible exactly when it needs to be, and reversible everywhere else.

User lifecycle

A user only exists in fremforge as a member of one or more orgs. Removing them from an org is a per-tenant action; deactivating their entire account only happens when they’re removed from their last org.

Removal from a single org

You remove a member from your org either manually (the Seat-management section in Billing lists every member with last-activity, supports bulk-remove, and offers an opt-in auto-cleanup policy) or via SCIM deprovisioning from your IdP. The behaviour is the same either way:

  1. The Forgejo org_member.removed event fires.
  2. fremforge recomputes seats_in_use from Forgejo’s live member list and emits a tenant.seat.released audit event. The freed slot becomes immediately available to your next invite or SSO-JIT sign-in.
  3. Any org-scoped PATs the leaving user holds for this tenant are revoked.
  4. If this was their last org, the orphan-deactivation flow below kicks in.

Your contracted seat cap is never reduced by cleanup. seats_in_use drops; the cap stays whole. To reduce the cap, use the Billing page (seat reductions take effect at the next renewal — annual prepayment is non-refundable per terms §4.7).

Auto-cleanup of stale accounts

The Members page lets an Owner set an inactivity threshold (default: disabled). Once set, a daily job removes members whose Forgejo last_login is older than the threshold. Owners, Operator accounts, and Bot accounts are never auto-removed; a per-tenant floor (default 30 days) prevents accidentally kicking members on long absences. The daily job is capped at 50 removals per tenant per day — a safety brake so a misconfigured policy can’t wipe a whole org in one tick.

Orphan deactivation (last org)

When the per-tenant removal above leaves a user with zero org memberships across all tenants, fremforge automatically:

  1. Sets prohibit_login=true on the Forgejo user (no more sign-in via SSO or local credentials).
  2. Revokes every Forgejo PAT the user owns, anywhere.
  3. Archives — does NOT delete — any personal-namespace repositories the user owns. Archive preserves history; the customer can restore by re-inviting before the 90-day hard-delete window closes.
  4. Records the deactivation in the orphaned_users book with cleanup_due_at = today + 90 days.

Re-inviting the user to any org within the 90-day window restores them: prohibit_login is lifted, PATs can be re-minted, and the orphan tombstone gets a restored_at stamp so the hard-delete job skips it.

Hard delete (orphan +90 days)

A daily job hard-deletes the Forgejo accounts that have been orphan-deactivated for more than 90 days and were not re-invited in that window. The Forgejo DELETE /api/v1/admin/users/{u} call removes the user account; the user’s archived personal repositories are removed transitively. The audit log retains a forgejo.user.orphan_hard_deleted event tied to the originally-triggering tenant for traceability.

After hard-delete, the username + account history are gone. There is no recovery beyond the 90-day window — provision a fresh user from your IdP if needed.

Tenant lifecycle

A tenant transitions through five states. The state machine is monotonic except for the cancellation_scheduled → active undo path and suspended → active reactivation path.

StateTriggerWhat it means
trialSignupFirst 30 days, no card required. Trial-suspension job moves to suspended if not converted by trial_expires_at.
activePaid plan startsNormal operation.
suspendedDunning past T+14, or trial expiredWrite access is paused; read + export + reactivate still work.
cancellation_scheduledCustomer initiated cancelService continues until cancel_effective_at. Undo lifts back to active any time before that date.
cancelledcancel_effective_at reachedForgejo org goes read-only. 60-day export + reactivate window.
deletedcancel_effective_at + 60dForgejo org and PII permanently destroyed. Certificate of destruction issued.

Cancellation initiated

When you click “Cancel” on the Billing page:

  • The status moves to cancellation_scheduled and cancel_effective_at is set (monthly term: +30 days; annual term: the prepaid commitment end-date — annual is non-refundable per terms §4.7).
  • Any in-flight overage is settled to the active payment method.
  • The Mollie SEPA mandate is revoked so no further automatic charges fire.
  • A confirmation email lands at every recipient in billing_emails. The Billing page now shows a countdown banner with the read-only-ends date, an export-data CTA, and an undo button.

The undo button is available right up until cancel_effective_at — and again during the 60-day read-only window via the reactivate button (a fresh payment method is set up through the standard checkout).

Cancellation effective (+0 days)

At cancel_effective_at the daily cancellation-effect job flips the status to cancelled and archives every repository in the Forgejo org (read-only). The org itself stays, the data stays — write access is paused everywhere except Billing (for reactivation) and Data export (for portability).

Permanent deletion (+60 days)

Sixty days after the cancellation took effect, the same daily job performs the physical erasure. Gates the deletion will respect — and any one of them blocks the purge for that day:

  • legal_hold = true on the tenant (an operator-set preservation flag for legal-hold preservation orders).
  • Any export job still queued or running.
  • Any in-flight payment (Mollie webhook status open, pending, or authorized).

When all gates clear, fremforge:

  1. Emits the tenant.physically_deleted audit event (this happens BEFORE the row mutation so the audit chain captures the action under the still-valid tenant scope).
  2. Revokes the per-tenant Renovate bot account and revokes its PAT.
  3. Purges any queued webhook-delivery jobs for the tenant from the Redis queue.
  4. Calls Forgejo DELETE /api/v1/orgs/{slug} — the org goes; repositories, LFS objects, and the org’s full state cascade.
  5. Deletes the Mollie customer record.
  6. Nulls the tenant’s PII columns (display name, billing contact email, billing emails, Mollie/Dinero references, VAT number), sets status = 'deleted', sets deleted_at = now.
  7. Generates the certificate of destruction PDF (see below) and archives it in OBS under the legal-hold-protected bucket. Emails the certificate to the captured billing-contact address.

Certificate of destruction

A one-page PDF generated at physical-delete time and emailed to the billing contact. It records:

  • The org slug, display name, VAT number, and tenant identifier.
  • The cancellation-effective date and the permanent-deletion date.
  • The categories of data destroyed (Forgejo content, tenant configuration, scoped member accounts, audit payloads, billing PII).
  • The residual retention carve-outs that still apply (see below).
  • A SHA-256 anchor hash that pairs the certificate with the tenant.physically_deleted audit event for cross-verification.

If you need a re-issued copy later, the archive lives in a WORM-locked OBS bucket — email compliance@frem.sh and we’ll re-issue from the archive.

Retention windows (DPA §9)

Time from cancellationWhat happens
+0 days (cancel_effective_at)Status → cancelled. Forgejo org archived, read-only. Export + reactivate available.
+60 daysStatus → deleted. Forgejo org physically deleted. PII nulled. Mollie customer removed. Certificate of destruction issued.
+90 daysAudit-event PII pseudonymisation runs against rows where deleted_at is at least 90 days old.
+120 daysBackups containing the tenant are purged. Total termination-to-backup-clear: 120 days.

Residual carve-outs

Some categories of data have statutory retention that survives the +60d primary deletion. These are documented in DPA §9 and reflected on the certificate of destruction:

  • Accounting records (invoices, voucher attachments): retained 5 years per Bogføringsloven §10.
  • Audit log metadata: retained 3 years, pseudonymised at +90d; no repository content, no PII.
  • WORM-locked compliance objects (audit-chain anchors, the certificate-of-destruction PDF itself): retained until the lock period expires. Documented carve-out in DPA Annex §9.

Legal hold

Operators can set legal_hold = true on a tenant in response to a preservation order (subpoena, GDPR investigation, dispute). While set, the cancellation-effect job skips the tenant entirely — no physical delete, no PII null-out, no backup purge. The flag is operator-only; customers cannot toggle it.

Common questions

My seat shows used even though the member left. Forgejo’s member list filters out users with prohibit_login=true, so a deactivated last-org user doesn’t count against your seats_in_use. If your number looks stale, the nightly seat-cap-drift-check job recomputes it.

Will downgrading the seat cap refund me? No — seat reductions take effect at the next billing cycle (monthly) or annual renewal (annual). Annual prepayment is non-refundable per terms §4.7. See Billing for the seat-cap form.

Can I get my data after the 60-day window? No. By the time you read this, it’s already gone (Forgejo org, repos, LFS, members, PII). Backups containing it are purged at +120 days. Plan exports during the read-only window — the Billing page surfaces an export CTA exactly for this reason.

Does GDPR Article 17 erasure happen at cancellation or at +60 days? The +60-day mark is when primary storage is wiped. Backups follow within another 30 days (+90 total from deleted_at, or +120 from cancellation). The certificate of destruction is the artifact you can show your regulator.

What if I cancel by mistake? Click the undo button on the Billing page — it works the full grace window (until cancel_effective_at). After that, the read-only window gives you 60 more days to reactivate via the standard checkout. After +60d there is no recovery.