- Shipped
- August 19, 2026 at 7:54 AM UTC
- Author
- Kamo
- Commit
- 15f942a
A synthetic account — a test artifact, a seeded fixture, or a signup judged to be automated — now carries `users.is_fake`, and the platform stops acknowledging it. Deleting such an account is not a real option: a user row is the root of seven satellite tables, every Member it owns and every Organization it created, which in turn own domains, features, email templates, roles and storage snapshots. Deleting means cascading through all of that or leaving orphans, and it destroys the evidence of whatever the account did. A boolean is reversible and costs one column. The flag lives on the USER, not on the Member or the Organization, because ownership is the only edge that reliably reaches the derived data: `orgs.user_id_owner` and `members.user_id` both point back here, so one flag answers "is this row's originator synthetic" for all three spines. Flagging the org instead would leave the account able to sign in and create more. What the flag reaches, and why each one is here rather than in the caller: - `UserRepository.findSession` and the auth projection: the flagged account resolves to zero rows, so it reads as non-existent rather than as blocked. - `MemberRepository` / `TeamMemberRepo`: excluded from org member listings and from both directory searches, alongside the System User that was already filtered there. - **************** the enumeration choke point nine call sites share. `getOrganizationById` is deliberately NOT filtered — it backs writes and it is how the admin surface reaches a flagged org to unflag it. - **************** a flagged org stops resolving by host, so it cannot keep accepting real signups under its own register subdomain. - **************** filtered in the query rather than in the watcher, because the sweep takes a fixed batch every minute and a domain nobody owns would otherwise hold one of those slots forever, probing someone else's DNS. Every one of these uses an explicit aliased LEFT JOIN. A bare `o.owner.isFake` path in a WHERE clause makes Hibernate add a second, implicit INNER join, which would silently drop every row whose owner is null — and for `findByDomain` that is login resolution. The finders above are additive; the identity finders on UserRepository are left unfiltered on purpose. Most of them are load-bearing for uniqueness pre-checks as well as for lookups, and `USERS.EMAIL`/`ALIAS` are unique per security-provider — so a registration duplicate-check that could not see a flagged row would sail past its own guard and die on a constraint violation instead. Also adds REGISTRATION, REGISTRATION_REJECTED, EMAIL_VERIFIED and ORG_CREATED to AccessLogEventType. `system_access_logs` only ever held login, failed login and logout, so an account that registered, verified, took an auto-login session and created five organizations produced zero rows — no event, no IP, no user-agent — and every detection rule keys off login events, leaving the whole register funnel unobservable. `users.is_fake` already exists in production: KamoInitializer's UserIsFakeMigration ran before this push, so no service can build against an unmigrated schema.