Skip to content

12 — legal-compliance design

Status: Draft Date: 2026-05-23 Owner: legal + frontend Parent: 2026-05-23-nowyourlink-master-design.md (binding — D7, D10, D29, D30, §4.1, §6) Source: OutBidThis Gesamtkonzept.pdf §9 (pp. 18–19)


Technical implementation guidance for legal/regulatory compliance — NOT legal opinion. Every template/clause MUST be reviewed by a qualified German lawyer (Fachanwalt IT-/Steuerrecht) before launch.

In: legal templates (Impressum, AGB, Datenschutz, Cookie Notice, Risk Notice); §14 UStG invoice fields; DSGVO map (Art. 13/15/17/20/30); retention; ELSTER + USt-IdNr. ops; B2B-only enforcement; ban-list under Art. 6(1)(f). (Disqus cookie consent was originally in scope but is removed — Disqus was dropped from the product, see §6.)

Out: legal advice; tax filings; content policy (03); identity mechanics (02); saga producing invoices (05).


Sources live in packages/legal/templates/ as Markdown with {{ company.* }} placeholders. Build-time packages/legal/render.ts interpolates from packages/legal/config.ts and emits .html (for 07) and .md (for 11). Locales: en/ + de/; German is binding per AGB §1.

FileAnchorRequired fields
impressum.md§5 DDG, §18 Abs. 2 MStVFirmenname, Rechtsform, Anschrift, Vertretungsberechtigter, Tel/Email, HRB-Nr., USt-IdNr. (§27a UStG), Verantwortlicher i.S.d. §18 MStV, EU-OS-Link
agb.md§§305 ff. BGBGeltungsbereich (B2B-only: “Unternehmer i.S.d. §14 BGB”), Vertragsgegenstand, Auktionsregeln (sealed-bid, 1 € min, 1 € increment, 00:00 UTC close, 24 h aktivierung), Zuschlag, Zahlung (Stripe, sofort fällig), Haftung (begrenzt auf Auftragswert), Strike-Regelung, Gerichtsstand (Anbietersitz), deutsches Recht, UN-Kaufrecht ausgeschlossen, expliziter Ausschluss §312g BGB Widerrufsrecht
datenschutz.mdArt. 13 DSGVO, §25 TTDSGVerantwortlicher, Zwecke + Rechtsgrundlagen (Art. 6(1)(b) Vertrag, (c) §147 AO, (f) Betrugsabwehr/Ban-List), Empfänger (Stripe, Cloudflare, Resend, Sentry), Drittland + SCCs, Speicherdauern (§4), Betroffenenrechte (Art. 15–21), Beschwerderecht
cookie-notice.md§25 TTDSGNur technisch notwendige First-Party-Cookies; keine nicht-erforderlichen Cookies (Disqus entfernt, §6) → keine Einwilligung nötig
risk-notice-advertiser.mdinformationalUWG-Hinweis, §6 TMG Kennzeichnungspflicht, Markenrechtsverletzungs-Haftungsverlagerung

packages/legal/config.ts keys: company.{name, legal_form, address, represented_by, contact_email, phone, register_court, register_no, ust_id}. CI gate (in 01) rejects empty values.


Stripe Invoicing produces the PDF per D10 + master §10.1; saga 05 populates fields.

§14 Abs. 4 UStG mandatory fields: (1) Name+Anschrift Leistender — Stripe account; (2) Name+Anschrift Leistungsempfänger — D1 user; (3) Steuernummer/USt-IdNr. Leistender — Stripe; (4) Ausstellungsdatum — Stripe created; (5) Fortlaufende, einmalige Rechnungsnummer — Stripe invoice.number (configure sequential, no gaps); (6) Leistungsbezeichnung — line item "Spotlight — {auction_id}"; (7) Leistungszeitpunkt — custom_field Leistungsdatum = auction_id; (8) Entgelt nach Steuersätzen — Stripe automatic_tax; (9) Steuersatz + Steuerbetrag, oder Befreiungshinweis — see §8; (10) §14b UStG Hinweis — Stripe footer: “Diese Rechnung ist 10 Jahre aufzubewahren (§147 AO).”

Conditional clauses (saga injects via custom_fields from D1 user country + VIES status):

  • DE → EU-B2B (VIES valid): “Steuerschuldnerschaft des Leistungsempfängers gem. §13b UStG / Art. 196 MwStSystRL. Reverse charge.” Both parties’ USt-IdNr. printed.
  • DE → Drittland: “Nicht im Inland steuerbare Leistung gem. §3a Abs. 2 UStG.”
  • DE → DE: 19 % USt regulär.

Credit notes use creditNotes.create (saga 05 §9) with identical mandatory-field set.


Enforced by per-store TTL + daily q-retention worker (packages/legal/retention/).

DatumFristRechtsgrundlageErzwingung
Rechnungen (PDF)10 Jahre§147 AO, §14b UStGD29 dual-write: R2 nyl-invoices-prod + hosted_invoice_url pinned in D1 invoices. R2 Object Lock (Compliance, 10y) once GA; pre-check head() write-once guard while Beta
Saga audit (saga_runs)10 Jahre§147 AO (Geschäftsbrief)no auto-delete
Verlierer-Bid-Datenmax. 6 MonatePDF §9.5; Art. 5(1)(c)nightly redact ad_id, keep anonymised aggregates
User-Daten bei Löschantragsofort anonymisiertArt. 17 DSGVOanonymizeUser() in 02
Ban-List (hashed_vat_id, hashed_email, strikes, banned_at)unbefristet (Admin kann streichen)Art. 6(1)(f) Betrugsabwehrown table; survives anonymizeUser (§5)
Conversion-Pixel-Events13 Monatedata minimisation; GA4-Mindestmaßq-retention daily
Push-Subscriptionsbis WiderrufArt. 6(1)(a)unsubscribe löscht sofort
Disqus-Archiv——Entfernt (2026-06-13) — Disqus aus dem Produkt entfernt (§6); kein Archiv mehr.
Magic-link Token15 mintechnisch erforderlichKV TTL
Sessions30 d sliding / 90 d capArt. 6(1)(b)KV TTL
VAT-validation attempts4 Jahre§147 AO (Buchungsbeleg)no auto-delete
Cookie-Consent-Log3 JahreNachweispflicht Art. 7(1)KV expirationTtl
Sentry-Logs (PII-frei)90 dDiagnoseSentry-Policy

D29 dual-write detail: in INVOICE_FINALIZE (saga 05 §3), after R2 PUT, saga also writes invoices(invoice_id, hosted_invoice_url, customer_id, auction_id, amount_cents, finalized_at, r2_key, hash_sha256) to D1. If R2 ever loses the object, hosted_invoice_url is canonical (Stripe retains ≥ 10 y).


/api/me/* routes are mounted on apps/api as Hono handlers (ADR-0001 / D40); the 08 dashboard at apps/web links to them. Session auth via the shared middleware; routes forward to 02.anonymizeUser() (etc.) via the USER_READ service binding.

Art.EndpointUIBackend
15 / 20GET /api/me/data-export08JSON-zip: users + bids + invoice links + saga + pixel; emailed
17POST /api/me/delete-account0802.anonymizeUser(); ban-list preserved
21POST /api/me/object08admin reviews legitimate-interest objection
16PATCH /api/me/profile08VAT-ID change triggers VIES re-validation (02)
30internal snapshot10nightly vvt.md snapshot

SLA: 30 Tage (Art. 12(3)). Diagram in processes/12-invoice-retention-and-dsgvo-erasure.md.

Art. 17 vs ban-list — conflict resolution

Section titled “Art. 17 vs ban-list — conflict resolution”
  1. If unpaid invoices → block with legal.unpaid-invoices (Art. 17(3)(b)).
  2. 02.anonymizeUser(): overwrite email, company_name, vat_id, country with deterministic hashes; null Stripe ids; preserve created_at; clear sessions + magic-links.
  3. Insert ban_list(hashed_vat_id, hashed_email, strikes, banned_at, last_strike_at, reason_codes[], retained_under='Art.6(1)(f)') — survives anonymisation. Legal basis: berechtigtes Interesse Betrugsabwehr; documented in datenschutz.md.
  4. Confirmation mail with Art. 14 transparency notice if ban-list row preserved.
  5. Audit row in user_admin_actions (immutable; owned by 02).

Future registration with same VAT-ID hash → 02 /register returns legal.banned-entity. Admin override in 10.


Section titled “6. Cookie consent — optional GA4 (2026-09-13)”

The owner explicitly requested GA4 with consent and conversion funnels on 2026-09-13. This updates the earlier no-banner assumption; Disqus remains removed. Existing Cloudflare analytics remains cookieless. GA4 is optional and must remain inactive until a valid production measurement ID is configured and the operator has approved Google processing terms, transfer arrangements, and the privacy disclosure. This implementation request does not attest that those prerequisites are complete.

Use basic consent mode: no Google script or request before explicit analytics opt-in. Reject and withdraw must be as accessible as acceptance; keep advertising purposes denied. Store the analytics choice for 180 days and cap GA cookies at 180 days. Withdrawal stops collection and removes GA cookies. Do not reinterpret the retired Disqus functional consent as analytics consent. Honour DNT and Global Privacy Control.

Only sanitized page paths and allowlisted funnel events may be sent. Exclude query strings, fragments, identifiers, bid amounts, creative content, and payment information. Do not label a magic-link request as successful login or registration. GA4 property retention, enhanced measurement settings, key events, and delivery verification remain separate activation checks; this document does not claim they are configured.

The retired Disqus consent log is not a GA4 consent-proof implementation. The owner must approve the consent evidence and retention approach before activation. No legal approval is asserted by this specification update.


Three layers:

  1. Registration (02 /register) — company_name + country required; vat_id required for EU; non-EU → admin queue (D7). Form copy: “Nur Unternehmer i.S.d. §14 BGB. Verbraucher können sich nicht registrieren.”
  2. AGB §1 — Vertrag nur mit Unternehmern; Widerrufsrecht §312g BGB ausgeschlossen; checkbox at registration writes users.b2b_acknowledged_at.
  3. Invoice — Stripe Customer tax_exempt: 'none' | 'reverse', never 'exempt'; never reference “Verbraucher”.

Widerrufsrecht does not apply. Self-serve deletion uses Art. 17 DSGVO (§5).


automatic_tax: { enabled: true } on invoices.create (05 §4).

KonstellationStripeInvoice text
DE → DE19 % UStregulär
DE → EU-B2B (VIES valid)0 %, reverse_charge”§13b UStG / Art. 196 MwStSystRL — reverse charge”
DE → EU-B2B (VIES ungültig / vies_degraded_pending)19 % DE-USt (Stripe-Fallback)regulär — bidder im Dashboard gewarnt (08)
DE → Drittland0 %, nicht steuerbar”§3a Abs. 2 UStG nicht im Inland steuerbar”

Fallback for Stripe Tax error: if invoices.create returns automatic_tax.status='failed', saga raises NonRetryableError; admin manually finalises with correct tax. Stripe Tax at invoice time is the legal source of truth; VIES at registration is advisory (02 §4).


Founder pre-launch (not code): Gewerbeanmeldung / GmbH-HRB → Steuernummer → USt-IdNr. beim BZSt (Pflicht für Reverse-Charge) → ELSTER-Zertifikat → monatliche USt-Voranmeldung (§18 Abs. 2 UStG, Neugründer-Pflicht) → ZM monatlich bei EU-Reverse-Charge. Bookkeeping: Lexware Office oder sevdesk mit Stripe-Connector + DATEV-Export. Published under docs-site operations (11).


10. Pre-launch compliance checklist (PDF §9.7)

Section titled “10. Pre-launch compliance checklist (PDF §9.7)”
#ItemOwnerSpec
1Impressum, AGB, Datenschutz, Cookie Notice, Risk Notice — lawyer-reviewedfounder12 + 07
2§14 UStG fields verified on real test invoice (DE, EU-RC, non-EU)founder12 + 05
3USt-IdNr. issued; ELSTER live; ZM dry-runfounder12
4Art. 30 Verarbeitungsverzeichnis (vvt.md) populatedfounder12
5DSGVO endpoints (Art. 15/17/20) live + testedbackend08 + 10 + 12
6Disqus cookie consent + log verified — N/A (2026-06-13): Disqus removed (§6); no non-essential cookies, no consent logfrontend07 + 12
7B2B-Checkbox + AGB §1 at registrationfrontend02 + 12
8Ban-list survives DSGVO erasure (integration test)backend02 + 12
9R2 invoice retention dual-write verifiedbackend05 + 12
10Art. 28 AVV signed with all sub-processors (Stripe, CF, Resend, Sentry, Lexware/sevdesk)founder12

All 10 green in CI legal-gate before public deploy.


SpecTouchpoint
02-identity-onboardingB2B form copy, D30: VIES-degraded cap = 100 € (not 200 €) — confirmed here, 02 §4 must update before launch; ban-list write on anonymizeUser; DOI text
03-ad-moderationcontent-compliance (HWG, UWG, Markenrecht); risk-notice ack at upload
05-payment-sagainvoice custom_fields, reverse-charge clauses, R2+D1 dual-write, credit-note PDFs
07-public-site-archiverenders packages/legal/ templates; footer legal links (no cookie-consent gate — Disqus removed, §6)
08-advertiser-dashboardDSGVO self-serve endpoints; conversion-pixel privacy text; ban-status display
10-admin-consoleDSGVO handling UI; ban-list management; manual VIES override; retention-job monitor
11-docs-sitepublishes ELSTER/ZM ops notes under /operations/legal/

  1. §34b GewO Versteigerer-Erlaubnis — required for digital ad-slot auction? PDF §9.2.
  2. §18 Abs. 2 MStV Verantwortlicher — required given UGC (advertiser creatives)? (Disqus comments removed — §6.)
  3. HWG + UWG auto-reject categories in 03 (Rx, Glücksspiel, Finanzversprechen).
  4. Markenrecht-Haftungsverlagerung AGB clause — §307 BGB AGB-Kontrolle.
  5. §25 TTDSG vs CF Web Analytics — cookieless but reads referer; consent needed?
  6. Stripe Tax wrong rate — liability vs §14 Abs. 4 Nr. 8 UStG.
  7. AVV completeness for CF sub-processors (R2, D1, Workers, DOs).
  8. Strike ban duration — indefinite vs Verhältnismäßigkeit; sunset (5 y)?
  9. Zuschlag-Moment — Vertragsschluss bei auction.closed oder invoice.payment_succeeded?
  10. SCC + TIA depth für Stripe US / Sentry US.

  1. All five legal documents render at /legal/* in EN + DE; German binding.
  2. Finalized invoice for each tax constellation (DE, EU-RC, non-EU) carries all 10 §14 UStG fields + correct conditional clause; snapshot-tested.
  3. Deletion of user with strikes ≥ 1 anonymises users, preserves ban_list, blocks re-registration with same VAT-ID hash.
  4. Disqus iframe does not load on first visit; loads only after consent; consent_id in KV with 3 y TTL. Retired (2026-06-13) — Disqus removed (§6); no non-essential cookies, so no consent gate or log.
  5. q-retention dry-run purges loser bids > 6 m, pixel events > 13 m. (Disqus archive purge retired — Disqus removed, §6.)
  6. D29 dual-write: every finalized invoice has R2 object AND hosted_invoice_url in D1 within 2 min.
  7. Art. 15 export < 30 s, includes all §5 categories.
  8. Pre-launch §10 checklist all 10 green in CI legal-gate.

  • Layout: packages/legal/{templates,config.ts,render.ts,retention/,__tests__}.
  • CI gate pnpm legal:check: config.ts non-empty; each template references its required fields; German version matches hash in APPROVED_HASHES.json; no unresolved TODO(lawyer): … markers in main.
  • No inline legal text in Astro. Use <LegalDoc slug="agb" locale="de" /> island importing packages/legal.
  • No final legal copy without lawyer sign-off.
  • Retention worker: cron 0 3 * * * UTC; D1+KV+R2 bindings; idempotent; emits legal.retention.executed to Sentry.
  • Sentry tags: subsystem=legal-compliance, retention_job, dsgvo_request_type.
  • No PII at debug level. Use hashUser() from 02 for breadcrumbs.
  • This spec is implementation guidance only. Final legal review by qualified German counsel is required before public launch.