Annex 2: Technical and Organisational Measures
Last updated 17 August 2026
This annex forms part of the data processing agreement between the Customer and Unbogify GmbH, Zelterstr. 10, 10439 Berlin, Germany, operating the service under the name Steerd. Section 4 of that agreement incorporates it.
It is written as a description of what the system does, not as an assurance of what it achieves, and where a measure is absent it says so. The Known gaps section at the end is part of the annex rather than an appendix to it: a customer is entitled to know what is not in place, and a claim a security questionnaire would catch is worth less than an admission that it would not.
On the file names in this annex. Many measures below name the file in the Processor's codebase that implements them. Those names are not terms of this annex and impose no obligation; they are verification pointers, kept because a specific claim is worth more than a general one and because they let the Processor re-check its own statements when the code changes. A Customer exercising the audit right in section 10 of the agreement can have any of them evidenced. They are deliberately not hyperlinks: the repository is private, and a link a reader cannot open has no place in a contract.
- Controller: the Customer, being the freelancer or company using Steerd
- Processor: Unbogify GmbH, Zelterstr. 10, 10439 Berlin, Germany
- Service: Steerd, an all-in-one business platform for freelancers and small businesses:
contacts and pipeline, CVs, time tracking, travel expenses, documents, and invoicing including e-invoicing. Managing client relationships is one module of it, not the whole product. The service description is what scopes the processing, so it names the whole list rather than the part the product started as.
1. Pseudonymization and encryption (Art. 32(1)(a))
Encryption in transit. All traffic is HTTPS. TLS terminates at the reverse proxy in front of the application; certificates are provisioned and renewed automatically. Mailbox connections require TLS (apps/api/src/lib/email/sources/imap.ts).
Encryption at rest, secret material. AES-256-GCM with a random 96-bit IV per operation and the authentication tag stored alongside (apps/api/src/lib/crypto/aead.ts). Keys are per-team data encryption keys, themselves wrapped by a master key (apps/api/src/lib/crypto/team-keys.ts, apps/api/src/lib/crypto/master-key.ts), which makes it possible to render one customer's encrypted data unrecoverable without touching any other customer's. Encrypted this way:
- IMAP passwords and mailbox settings, sealed as one blob (mailbox_credentials)
- Gmail and other OAuth tokens (mailbox_credentials, integration_credentials). The better-auth
account table has plaintext token columns by default; a write hook forces all five to null on every account write, so no OAuth token is persisted there at all (apps/api/src/lib/auth-account-tokens.ts)
- Documents, entity attachments, travel receipts and finalized invoice PDFs, encrypted before they
reach object storage (apps/api/src/routes/files.ts, routes/attachments.ts, services/trip-receipts.ts, services/invoice-finalize.ts)
A decryption failure (wrong key, tampered ciphertext) raises rather than returning partial data.
Four details that a security questionnaire asks about specifically:
- The primitive takes and returns bytes, so a 25 MB file is never inflated into a string.
- The full 128-bit GCM authentication tag is required: a truncated tag is rejected rather than
silently verified against fewer bits.
- The wrapped key binds **both the master key generation and the owning team id as additional
authenticated data**, so a wrapped key can be replayed neither under a different master key generation nor into another team's row (master-key.ts).
- Rotating the master key re-wraps the team keys without re-encrypting a single blob
(apps/api/src/jobs/rotate-kek.ts, rotate-team-dek.ts). Master keys are provisioned per environment, so staging and production never share one.
What "encrypted at rest" does not cover, stated plainly. Ordinary CRM rows in Postgres (contacts, projects, invoices, time entries) are stored in plain columns and are protected by access control, not by application-layer encryption. Whether the underlying disk volumes and the Hetzner object-storage buckets carry provider-level encryption at rest is not established in this repository, and is stated that way rather than letting "encrypted at rest" imply more than it covers. The S3 backup bucket is a separate question and is confirmed AES-256 server-side encrypted (§4).
Nor does it cover every object in object storage, and this one is easy to read the wrong way. Application-layer encryption applies to the blob namespaces listed above. It does not apply to the uploads namespace, whose objects are written and stored as plaintext by us, by design (apps/api/src/routes/uploads.ts, services/contacts-vcard.ts; docs/ARCHITECTURE.md §4.8, which names it "the plaintext uploads namespace"). The five purposes it carries are account avatars, contact avatars, employee avatars, CV photos and invoice logos (apps/api/src/lib/uploads-purposes.ts), so it holds photographs of the customer's own data subjects. Generated export archives are plaintext too (apps/api/src/lib/files/export.ts), which is the point of §6's note that they hold data subjects' data and of the 30-day window over them. Cached favicons live under their own favicons/ prefix, also plaintext, and are not personal data. All of this is protected by access control and by the same absent provider-level assurance as the Postgres rows above, which is why it belongs here rather than in a footnote. Tracked as Known gaps, item 13.
Hashing. API keys and CardDAV application passwords are stored only as SHA-256 hashes and compared in constant time; the plaintext is shown once at creation and is not recoverable afterwards (apps/api/src/db/schema/api-keys.ts). Account passwords are hashed by the authentication library.
Secrets are never logged. Encryption keys are read lazily and excluded from log output.
2. Confidentiality (Art. 32(1)(b))
Tenant isolation. Every application table carrying customer data is scoped by team_id, and every query filters on the caller's team. Cross-tenant reads are additionally guarded by validateFKRefs, which re-checks that any referenced record belongs to the caller's team before a write.
Access control. Role-based permissions per team member, with an explicit grantable-permission allowlist (apps/api/src/db/schema/rbac.ts). Administrative views such as the audit log are restricted to team administrators; members receive 403.
Authentication. Session-cookie authentication for the application, plus scoped API keys for programmatic access. Rate limits apply per IP, per key and to failed authentications.
Server-side request forgery defense. User-supplied mailbox hosts are DNS-resolved and rejected if they resolve to private, loopback, link-local or carrier-grade NAT ranges, including the cloud metadata address. The connection is then pinned to the validated IP so a second DNS answer cannot redirect it (apps/api/src/lib/net/net-guard.ts).
Third-party assets are self-hosted. Fonts and country flags are served from our own origin, so loading the application makes no request to a third-party CDN.
Separation of environments. Staging and production have separate databases, separate encryption keys and separate object-storage buckets in separate Hetzner projects, so a credential from one cannot reach the other's data.
Network exposure. Postgres and Valkey are managed by the hosting control plane and publish no public port, and ufw on the host is default-deny inbound with SSH the only exception (docs/deployment-coolify.md).
Secret management. Operator secrets are held in a self-hosted password vault and fetched at the moment of use rather than being written to disk. There are deliberately no plaintext secrets in environment files on anyone's machine: tooling reaches the vault through a credential hook, and configuration management fetches at converge time. The production master key that wraps every team's encryption key is escrowed in that vault and is not held in the database, which cuts both ways: a database restore alone cannot recover it, so a stolen database backup is not sufficient to read encrypted data, and losing the key would make that data permanently unreadable.
Staff access. The one part of this annex that describes people rather than code, and the first thing a customer security review asks about.
Administrative access to production means two things, and they collapse into one: shell access to the host, and login to the hosting control plane. The control plane holds every production environment variable and can execute inside the running containers, so control plane access is effectively full database access. There is no narrower "production database login" to describe, and describing one would overstate the separation.
Access is individually attributable: the production hosts carry one named key per person alongside the deploy key the control plane uses for itself. The database and cache publish no public port, and the host firewall is default-deny inbound with shell access the only exception.
Three things about staff access are not in place, and they are stated rather than omitted:
1. One credential on a production host is unaccounted for. An access key present on the host is not attributable to a named person, and the infrastructure configuration does not reconcile that list, so the live host is the only source of truth for it. It has not been removed. 2. Multi-factor authentication is not confirmed on the control plane login or on the hosting and cloud provider consoles. Whether it is enforced is not recorded, so this annex does not claim it. 3. Confidentiality commitments under Art. 28(3)(b) exist for the people with that access, but the record of who those people are is inferred from access configuration rather than maintained as a document.
The first of these is the one a security review will care about most, and it is disclosed here for that reason rather than left to be found. It is also the reason section 10.6 of the agreement volunteers that the Processor holds no ISO 27001 or SOC 2 certification: a formal access-management policy is exactly what such a certification would require, and none exists yet.
3. Integrity (Art. 32(1)(b))
Append-only audit trail. Sensitive team actions (API key lifecycle, role changes) are written to audit_log inside the same transaction as the action itself, so a failed audit write rolls the action back and cannot leave a silent gap (apps/api/src/services/audit.ts).
Separate security event log. Account-level security events (password and email changes) are recorded in auth_events, deliberately separate from the tenant-facing audit trail and never surfaced in it.
Export integrity. Data exports carry a manifest.json with a SHA-256 per file, and the export can be re-verified end to end from the archive alone (apps/api/src/lib/files/export.ts).
4. Availability and resilience (Art. 32(1)(b), (c))
Application containers are stateless and restartable; health endpoints gate traffic, so a container that fails to start never receives requests. Object storage holds a mix of encrypted blobs and plaintext objects, and §1 says which is which; this paragraph used to claim it held encrypted blobs only, which was never true of avatars, CV photos or export archives.
Database backups. The production database is dumped nightly at 03:45 UTC and the dump is uploaded to object storage held with a different provider from the database itself: the database runs on Hetzner infrastructure, the backups land in Amazon S3 eu-central-1 (Frankfurt, Germany). Both are in Germany. The separation that matters here is the provider rather than the jurisdiction: a backup on the same provider as the primary does not survive losing that provider.
Backup storage properties, all enforced in infrastructure-as-code rather than by convention:
- Retention 90 days, owned by a storage lifecycle rule rather than by the tool that writes the
backups, so a misconfigured producer cannot shorten it.
- Encrypted at rest (AES-256, server-side).
- Not publicly accessible, and reachable only over TLS.
- Versioned, so an overwrite does not destroy the previous copy.
- The credential that writes backups cannot delete them. It holds no delete permission at all,
verified against the live key. A compromised production host can therefore add to the backup history but cannot erase it.
What this does not provide, stated plainly. These are nightly logical dumps, not continuous archiving: there is no point-in-time recovery, so the worst-case data loss from a database failure is the time since the last nightly dump. Backups of uploaded documents and attachments are a known gap, because that storage is versioned but not separately backed up (Known gaps, item 6).
Backups are verified to be real artifacts, and restoring was rehearsed on 10 August 2026. A backup is not treated as done because the job reported success: for both environments a stored backup has been retrieved from the backup storage and checked to confirm it actually contains the database's tables rather than being a structurally valid but empty file, which is the failure mode that otherwise goes unnoticed. Beyond that, the production backup has now been restored end to end, from the artifact in backup storage rather than from a copy kept on the server it protects: the row counts came back matching live production, the application started against the restored database and served a request, and the encryption-key path was exercised as part of the same test, because a restore that brings back rows nobody can decrypt is not a restore. A wrong key was tried as a control and correctly failed. What Art. 32(1)(c) asks about is therefore no longer written-only: the restore path that was rehearsed on that date has been executed and measured. It is a record of one test on one date, not a standing guarantee, and the paragraph below says which parts of the capability the test did not reach.
Two limits on that statement, both deliberate. The rehearsal restored into a separate scratch database and not over a live one, so the recovery step that overwrites a running system remains written rather than tested. And it was performed while the production database was still small, so it establishes that the procedure works and not how long it takes at customer volume. Both are recorded in Known gaps, item 4.
Backup monitoring is partial. A backup that fails raises an alert. A backup schedule that silently stops running does not. Nothing currently detects the absence of a backup, only its failure (Known gaps, item 5).
5. Testing and evaluation (Art. 32(1)(d))
Continuous integration runs on every pull request and is a required check on main. It lints the whole repository, and it type checks and tests the affected packages rather than everything: the build tool compares against the pull request's base and runs the packages that changed plus those that depend on them, so a change to shared code still tests the API and the web app. Say "affected" rather than "the full suite", because the two differ in one case worth disclosing: a change that touches no package at all, a workflow or documentation edit, resolves to zero packages and reports a green test check having run nothing. The suites it draws from are 3,141 API tests across 339 files and 1,642 web tests, counted on 09.08.2026 with vitest list, which enumerates tests without executing them.
The same required check carries gates that block a merge introducing destructive database migrations without an explicit acknowledgement, read-blocking migration locks without a written decision, a stale public API schema snapshot, or a break in the migration ordering invariant.
Changes go through pull requests, and current practice is to review one before merging it. That is a practice, not an enforced control, and the distinction is the point of writing it this way. Branch protection on main requires the CI and Images checks to pass; it does not require an approving review, does not require conversation resolution and does not apply to administrators (read from the branch protection API on 09.08.2026). So the enforced part is the checks, and the review part rests on how the team works. "Every change is reviewed before merge" would be a universal about the past that nothing here records and an auditor could ask us to evidence.
Not in place, and worth stating rather than being asked: no penetration test, no ISO 27001, no SOC 2, no external security audit, and no formally scheduled review of these measures. Art. 32(1)(d) asks for a process for regularly testing and evaluating effectiveness, and an annual review with a date and a signature is the minimum that satisfies it. No such review is yet scheduled.
5a. Attack surface controls
- SSRF defense on user-supplied mail hosts and on the favicon fetcher: DNS resolution, rejection
of private, loopback, link-local and CGNAT addresses including the cloud metadata address 169.254.169.254, then the connection is pinned to the validated IP so DNS rebinding cannot redirect it, with the hostname kept only for TLS SNI (apps/api/src/lib/net/net-guard.ts).
- Rate limiting per IP, per API key and on failed authentication, backed by Valkey with an
atomic fixed window (apps/api/src/lib/rate-limit.ts).
- CORS is an explicit allowlist, not a wildcard.
- Dependency management: pnpm audit runs **weekly on a schedule, and reports rather than
gates** (.github/workflows/audit.yml, Mondays 06:15 UTC). It opens and rewrites a single tracking issue; it does not block a merge, deliberately. The reason is about when it runs, not about what it can see: the finding it exists to catch is an advisory published against a lockfile nobody touched, and a merge-time check would only notice it on the next pull request, which on a quiet week never comes. A pull-request gate for newly introduced advisories is a separate, deferred question. The override block in which every entry is a security fix with its reason written down is in docs/ARCHITECTURE.md §9.8.
Stated limitation. The rate limiter fails open when Valkey is unreachable (apps/api/src/lib/rate-limit.ts), and logs at most once every 30 seconds, so a Valkey outage removes brute-force protection from the public API with almost no signal. That is a deliberate availability-over-security tradeoff, documented as such in docs/ARCHITECTURE.md §9.7 and tracked as a gap in section 3. A customer security questionnaire will ask; better to have said it here first.
5b. Logging and monitoring
Honest and short. The team-facing audit_log and the operator-facing auth_events table are append-only and are the durable record of who did what through the application, retained 365 and 90 days respectively (§6). Application logs are container stdout.
Error reporting exists, on the server side only. The API and the worker report unhandled server errors to a self-hosted receiver on the Processor's own infrastructure. Three capture points, and no others: a request whose error is about to answer 500 or above, a background job that has failed terminally rather than on an attempt that will be retried, and a failure that stops a container starting (apps/api/src/lib/observability/report.ts). A 4xx is dropped deliberately, which means every authorization denial is invisible there and lives in audit_log instead.
What travels with an event, and the one qualification that matters. The structured event carries the exception type, message and stack trace, the route pattern, the user and team id and the release; it deliberately carries no request body, no query value, no cookie, no Authorization or X-API-Key header, no email address, no IP and no local variables (sendDefaultPii: false plus a beforeSend scrubber in apps/api/src/instrument.ts). That guarantee is about the structured fields only. Console breadcrumbs from the run-up to the error travel with the event and are not scrubbed field by field, so anything a log line happened to contain reaches the receiver whatever the list above says. The application logs with a bare console, so this is the same data class the container's stdout already holds, for a shorter time and on our own hardware, but it is the honest limit on the sentence before it. Retention on the receiver is 30 days. The full field-by-field list of what is and is not sent is maintained by the Processor and is available on request.
The browser side reports too, into separate projects from the API's. The customer's browser posts to /monitoring on the application's own origin and the Processor forwards the report server-side, so no customer's browser ever contacts the receiver directly and there is no third-party request in their network log. The sent-versus-not-sent table for the browser is in the same list, and is stricter than the API's: console breadcrumbs are switched off at the source rather than filtered. Two limits are worth stating, because both are ways an error can still be invisible to us: a report travelling over the app's own origin is still a request an ad-blocker or a corporate proxy can drop, and nothing is captured at all where the application handled the error and showed the customer a message.
Everything else in this section is still absent: no centralized log retention, no alerting, no APM and no intrusion detection (docs/ARCHITECTURE.md §11, §9.7). Read "no alerting" as covering the application: the one alert that exists anywhere is the backup job's own failure notice (§4), which says nothing about the application and, as Known gaps item 5 records, does not fire when the schedule simply stops. Error reporting narrows the weakest measure in the annex; it does not close it, and no condition inside the product raises an alarm on its own. The section is stated in full because writing anything else would be a claim, and because a customer discovering it during an audit is worse than reading it here. What is missing is alerting, not reporting: both halves of error reporting exist and neither of them raises an alarm. Browser stack traces also arrive minified, because source maps are not uploaded to the receiver; that costs triage time rather than detection.
No monitoring vendor was added, and that was the point of self-hosting. The receiver runs on the Processor's own infrastructure in nbg1 (Nuremberg, Germany), under the same provider account that already carries the application, the database and object storage. No row joins §8, which is the Art. 28(4) subset of third parties. All three of these instances are operated by the Processor itself on its own fleet, and "the Processor" here means Unbogify GmbH: Nightly Build Group is a proposed trading name for that same company and not a separate legal person, so no further entity is interposed and none of the three is a subprocessor.
What did change is where some data rests. Error events sit on a second server: application runtime data otherwise stays in fsn1 (Falkenstein), while these land in nbg1 (Nuremberg). Both are in Germany, as are object storage and the AWS backups, so this is not a second jurisdiction; it is the first time data leaves the application host.
If a monitoring vendor is ever adopted instead it becomes a subprocessor, and section 8 and Annex 3 change with it, on the 30 days' notice section 5.2 of the agreement requires.
6. Data minimization and storage limitation (Art. 5(1)(c), (e))
Email. Steerd does not read a connected mailbox on its own. Messages are fetched only when the user opens the import dialog. An imported message stores the sender, the subject, the first 2,000 characters of the body as plain text and attachment metadata; attachment contents and full message bodies are not stored (apps/api/src/services/email-ingestions.ts). Gmail is requested with gmail.readonly as the only mail scope, and the callback verifies that scope was actually granted before activating the connection.
Uploads. Uploaded images are decoded and re-encoded, to WebP for every purpose except the invoice logo, which is PNG because the invoice renderer cannot decode WebP. Either way the re-encode strips EXIF and normalizes orientation, which removes camera and GPS metadata (apps/api/src/lib/storage/images.ts). Audit log and auth event detail fields carry non-sensitive metadata only, never secrets and never a raw new email address (docs/auth-events.md).
Counter-example, included because an annex that only lists wins is not an assessment, and kept after the fix because the fix took a launch deadline to force. The Gmail consent also requested calendar.events, which nothing in the codebase used (apps/api/src/lib/email/sources/google-config.ts). It was requested up front to avoid a re-consent when the calendar integration ships: a defensible product decision and an indefensible data-minimization one, because a user connecting Gmail granted calendar write access that nothing exercised. Removed 07.08.2026, when preparing the Google verification submission made the second reading the binding one: every requested scope has to be demonstrated in use.
Retention.
| Data | Window | Mechanism |
|---|---|---|
| Dormant free accounts: contacts, email ingestions, email activity metadata | 365 days of inactivity, after warnings at 30 and 7 days | apps/api/src/jobs/purge-dormant-accounts.ts |
| Team audit log | 365 days | apps/api/src/jobs/purge-logs.ts |
| Security event log (holds IP address and user agent) | 90 days | apps/api/src/jobs/purge-logs.ts |
| CardDAV sync tombstones | 90 days | apps/api/src/jobs/carddav-tombstone-gc.ts |
| Request idempotency cache | 24 hours | apps/api/src/lib/http/idempotency.ts |
| Orphaned storage objects | Reconciled daily | apps/api/src/jobs/files-reclaim.ts |
| Export archives (plaintext, hold data subjects' data) | 30 days from creation, not shortened by account deletion | apps/api/src/jobs/purge-exports.ts |
| Server error events (stack trace, route pattern, user and team id, console breadcrumbs) | 30 days | A retention setting on the self-hosted receiver (§5b) |
How these windows are enforced, and the limit of that. Every window above except the last is carried out by a scheduled task configured in the hosting platform rather than by the application itself, so the existence of the code that performs a deletion is not by itself evidence that the deletion runs. The error-event row is the same in the other direction: it is a retention setting on the receiver, and nothing in the application enforces it.
As at 09.08.2026 all seven scheduled tasks are present and enabled in both environments, and the three retention purges have each completed a run that reported applying its deletions. That is a statement about a date rather than a standing guarantee, and the Known gaps section records the part that is genuinely not covered: nothing raises an alarm when a schedule stops running, so a schedule that silently stopped would not be visible until someone looked. Each of these jobs deletes by age rather than incrementally, so a missed day is made up by the next successful run; a missed month would not be noticed by the system.
Two columns deliberately survive the dormant purge. projects.description and activities.content can hold the same 2,000-character email body excerpt, written when the user confirms an ingestion (apps/api/src/services/materialize.ts). They are left alone because by that point the text is the user's own business record and blanking it would silently mutate records they believe they authored. The full reasoning, the counter-argument, and the fact that this is an argued default rather than a settled one are recorded by the Processor and are available on request. Annex 1.5 of the agreement states the same thing, and the two must not diverge.
7. Assisting the controller (Art. 28(3)(e), (f))
Access and portability (Art. 15, 20). Self-service export from Settings produces a ZIP containing the team's records as JSON, one file per table, plus every uploaded document and attachment, with a verifiable checksum manifest (apps/api/src/lib/files/export-records.ts).
Erasure (Art. 17). Self-service account deletion removes the user's teams and their contents. Individual records are deleted on request in the product, and that deletion is a database delete rather than a flag: deleteContact removes the row, cascades five child tables, purges the attachments and drops the avatar object (apps/api/src/services/contacts.ts). An imported email can be erased individually (DELETE /api/v1/email-ingestions/:id).
One surface is an exception, and this paragraph used to deny it existed. contacts.archived_at and projects.archived_at are soft-delete columns, and a CardDAV DELETE from a phone archives the contact rather than deleting it (apps/api/src/routes/carddav.ts, archiveForDav). That is a deliberate safety decision, because a cascading hard delete is too destructive an outcome for an accidental swipe on a phone, and the card does disappear from the synced address book either way. It is still worth stating here: an erasure carried out from a synced device leaves the row, and a hard delete stays an action taken in the application.
Deletion or return at end of contract (Art. 28(3)(g)). Account deletion offers both: an export is produced and verified before the team's encryption key is destroyed, so the return happens first and the erasure is only then irreversible (apps/api/src/services/account-deletion.ts).
8. Subprocessors (Art. 28(4))
| Subprocessor | Purpose | Location |
|---|---|---|
| Hetzner Online GmbH | Servers, database, cache, and the self-hosted error-event receiver | Germany: fsn1 (Falkenstein) for the application and database, nbg1 (Nuremberg) for error events (§5b) |
| Hetzner Object Storage | Files, attachments, travel receipts, avatars, CV photos, invoice logos, export archives | Germany (fsn1) |
| Amazon Web Services | Nightly database dumps, container images | Germany (eu-central-1) |
| Amazon Web Services (SES) | Transactional email relay, so it sees every recipient address, subject and body | Germany (eu-central-1) |
| OpenAI Ireland Ltd | AI-assisted CV and document import, on user action only: the content of the one document the user submits | Ireland. OpenAI Ireland transfers onward to OpenAI OpCo, LLC in the United States to run the model |
Every row has a data processing agreement in force. Section 6.3 of the agreement offers the Customer a copy of the relevant instrument on request, redacted only as necessary to protect the confidentiality of third parties, and that is the mechanism by which the evidence is produced rather than by publication. Annex 3 is the authoritative list of subprocessors; this table describes the same providers from the measures side and must not diverge from it. Only two of these agreements took an act to conclude, the Hetzner data processing agreement and the OpenAI self-serve form. The rest are either incorporated by reference, in which case the agreement is already in force and there is no signature to give, or the Processor's own self-hosted instances, which are not third parties at all.
This annex lists only providers that touch data we hold on the customer's instruction. Stripe, Cloudflare and Google process personal data for Unbogify as controller (billing, bot protection on the marketing forms, website analytics). They are named in the Processor's privacy notice and in Annex 3's list of providers that are not subprocessors, and they are deliberately not here. Listing them would misstate the roles and give the Customer an objection right over processing that is not its data.
The Processor's own mail sender, hosting control plane and error-event receiver are not here either, for a third reason: they are self-hosted instances rather than third parties. The receiver is the newest of the three (§5b) and is the reason to keep this paragraph in view, because the alternative that was rejected, a hosted error-tracking vendor, would have put a row in the table above. They raise no Art. 28(4) question: Nightly Build Group is a proposed trading name for Unbogify GmbH rather than a separate legal person, so operating them interposes no further processor.
OpenAI is in the table, with effect from 07.08.2026, which is when AI-assisted import went live in production. The transfer basis for it is stated per recipient in Annex 3, and the transfer impact assessment the agreement offers on request has been carried out.
Every provider that holds the Customer's data as part of running the service processes it in Germany. The application, the database and the cache are in fsn1 (Falkenstein), object storage is in fsn1, the error-event receiver is in nbg1 (Nuremberg), and the database backups are in eu-central-1 (Frankfurt).
The one exception is the last row of the table, and it is deliberate rather than overlooked. AI-assisted import sends the single document a user submits to OpenAI Ireland Ltd, which is in Ireland and not in Germany, and which transfers onward to the United States to run the model. That is why this paragraph does not say "every subprocessor": it would be false of that row, and a sentence that is true of four rows and false of the fifth is worse than no sentence. Annex 3 states the location and the transfer basis per recipient, and the two must agree.
Transfers to the United States rest on the standard contractual clauses incorporated in each provider's own DPA. The in-product privacy notice landed on main on 09.08.2026, and it states SCCs, so the two now agree. This paragraph previously said the notice did not exist and that its arrival was "a claim to re-check at that moment rather than one to inherit from here"; that moment came and the re-check is this sentence. Re-check it again if either side is edited, and note that the notice is its own document rather than a copy of the steerd.io policy (it says so in its opening line), so the two can drift apart without any file appearing in both diffs. Whether any of them is additionally certified under the EU-US Data Privacy Framework has not been checked for Hetzner, AWS or Cloudflare, so it is not relied on for them: DPF would be a simpler basis and would mean "standard contractual clauses" names the more cumbersome of two available instruments.
OpenAI is not certified under the EU-US Data Privacy Framework. Checked on 07.08.2026 against the official participant list, as an active participant and as an inactive one, under every spelling. The search was control-tested against a provider known to be listed, so the negative result is a real one rather than a failed query. The standard contractual clauses are therefore the only Art. 46 route for that recipient, and Annex 3 names them for it.
The Processor's own transfer to OpenAI is in any event not a third-country transfer: the counterparty is OpenAI Ireland Ltd, established in the European Union, and the onward transfer to the United States is that company's own. Annex 3 states this per recipient.
Why Stripe is not in the table above. Stripe processes personal data for the Processor's own billing relationship with the Customer, for which the Processor is the controller rather than the Customer. Section 5.5 of the agreement excludes such providers from the subprocessor list, because listing one would misstate its role and would give the Customer a right to object to processing that is not the Customer's data. Stripe is named in the Processor's privacy notice, and Annex 3 lists it among the providers that are not subprocessors under the agreement. Unbogify GmbH remains the Customer's counterparty for the software itself.
9. Breach notification (Art. 33)
The Processor maintains a documented breach response procedure. It notifies the Customer of a personal data breach affecting personal data processed on the Customer's behalf on the terms and within the period section 8.2 of the agreement sets.
Known gaps, stated rather than hidden
An annex that overstates the system is worse than one that admits a gap, because the overstatement is a contractual promise.
1. Retention runs, and is evidenced by a date rather than by a guarantee. Closed 09.08.2026 (section 6): all seven scheduled tasks are present and enabled in both environments, and the three purges have each completed a run that applied its deletions. What this does not close is the monitoring half, which is item 5: nothing detects a schedule that has stopped running. So the correct reading of this item is that the retention windows were verified as running on a stated date, not that they are guaranteed to run. 2. No penetration test or external security audit has been performed. 3. No formal access-management policy exists for staff access to production (section 2). Who has access is now recorded and is individually attributable, but MFA is unconfirmed, confidentiality commitments are undocumented, and one SSH key on the production host is unaccounted for. 4. Backup restore is rehearsed, with two parts of it still untested. The full restore was executed on 10 August 2026, including the encryption-key path (section 4). What remains untested is a restore that overwrites a running database, and a restore at customer data volume: the rehearsal used a separate scratch database while production was still small, so the measured duration answers none of the questions that would be asked during an incident at customer volume. 5. Nothing detects a backup that stops running. Failures alert; silence does not (section 4). This is not hypothetical on this infrastructure: a different database's backup schedule once stopped for 49 days with failure alerting enabled the whole time, because nothing failed, nothing ran. 6. Uploaded files and attachments are not separately backed up. That storage is versioned, so an accidental delete or overwrite is recoverable, but there is no independent copy with a second provider as there is for the database (section 4). 7. There is no point-in-time recovery for the database, only nightly dumps (section 4). 8. Closed 07.08.2026: the AI import's transfer basis is settled, and one disclosure survives it. The item used to read that AI import sends document content to the United States under an unconfirmed mechanism. It does not. The Processor's counterparty is OpenAI Ireland Ltd, established in the European Union, so that transfer is EU to EU and Chapter V does not engage at the Processor's layer; the onward transfer to the United States is OpenAI Ireland's own, under its data processing addendum, and it remains liable for its own subprocessors. OpenAI is not certified under the EU-US Data Privacy Framework, so the standard contractual clauses are the only Art. 46 route for that onward transfer, which is what Annex 3 names.
What survives as a disclosure rather than a gap: content sent to that API is not used to train models, and is retained by the provider for at most 30 days for abuse detection, during which it is readable by the provider's staff and by specialist contractors performing abuse review. Annex 3 states this too, because it is the row customers ask about. 9. There is no application alerting and no intrusion detection (section 5b). Error reporting itself is no longer the gap: unhandled server errors from the API and the worker reach a self-hosted receiver, and browser errors reach it too, though browser stack traces arrive minified because source maps are not uploaded to the receiver. What is missing is everything that would make a report arrive at a person: no application condition raises an alarm on its own, and there is no centralized log retention. "Application" is the scope, and the only exception anywhere is the backup job's failure notice in item 5, which watches a job rather than the product. The practical consequence for Art. 33 is unchanged, and it is worth saying why reporting did not change it: a report exists only where something crashed, and access that succeeds crashes nothing. So the first signal of a breach is still likely to be a report from the Customer, and the Processor's breach response procedure says the same. 10. The rate limiter fails open on a Valkey outage, with almost no signal (section 5a). 11. Provider-level encryption at rest is not established for the Hetzner volumes and buckets (section 1). Ordinary CRM rows are in plain columns. 12. Closed 07.08.2026: the Gmail consent no longer over-requests. It had asked for calendar.events write access that nothing used; the scope was removed. The item keeps its number rather than being deleted, so that a reference to "item 12" in an earlier review still resolves to the thing it was about. 13. Avatars, CV photos, invoice logos and export archives get no application-layer encryption in object storage (section 1). It covers documents, attachments, travel receipts and finalized invoices; the uploads namespace and the generated export ZIPs are written as plaintext by design. Whether anything encrypts them below us is exactly what item 11 says is not established, so for these objects there is no layer we control and no layer we can evidence, and the photographs a customer holds of their own contacts and employees are in that set. Found by the 09.08.2026 end-to-end pass, which is also when this annex stopped claiming that object storage held encrypted blobs only. 14. Review before merge is practice, not an enforced control (section 5). main requires the CI and image checks and no approving review, so nothing in the platform would stop an unreviewed change. Stated because Art. 32(1)(d) is about a process we would have to demonstrate, and a claim of an enforced gate is one an auditor can test in a minute.