Dieses Register dokumentiert alle Datenfelder, die im Boncard-Kundenexport enthalten sind. Es wird gemäß Art. 26(b) Data Act vor Vertragsabschluss veröffentlicht, damit Kunden und Zielanbieter die Migration planen können. Die Export-Endpoints und Anleitungen finden Sie unter /data-portability.
1. Schema-Katalog
Alle Datenstrukturen, wie in der internen Schema-Truth-Datei src/lib/db/types.ts definiert. Jedes Feld mit Typ und Beschreibung.
Customer Customer (End-Kunde des Tenants)
| Feld | Typ | Beschreibung |
|---|
| id | string | nanoid(16) primary key |
| barbershopId | string | Tenant scope (foreign key to Barbershop.id) |
| firstName | string | Customer first name (required) |
| lastName | string? | Customer last name (optional) |
| phone | string? | Phone number, E.164 preferred (optional) |
| email | string? | Customer email address (optional) |
| birthday | string? | ISO date YYYY-MM-DD (optional) |
| notes | string? | Free-text notes by owner/staff (optional) |
| gdprConsent | boolean | Art. 6(1)(a) GDPR base consent |
| marketingConsent | boolean | Marketing email consent (double opt-in) |
| birthdayConsent | boolean | Birthday greeting consent (double opt-in) |
| marketingConsentConfirmedAt | string? | ISO timestamp of marketing double opt-in |
| birthdayConsentConfirmedAt | string? | ISO timestamp of birthday double opt-in |
| unsubscribeToken | string? | OMITTED from export (live credential) |
| lastMarketingEmailAt | string? | ISO timestamp of last marketing email sent |
| lastBirthdayEmailAt | string? | ISO timestamp of last birthday email sent |
| consentGivenAt | string? | ISO timestamp when consent was given |
| consentSource | enum? | CARD_ACTIVATION_FORM | ADMIN_MANUAL_ENTRY | IMPORT | PUBLIC_WEB_FORM |
| consentIpAddress | string? | IP address captured at consent (Art. 7(1) proof) |
| consentUserAgent | string? | User agent captured at consent |
| isAnonymized | boolean | True if personal data has been erased (Art. 17) |
| createdAt | string | ISO timestamp record created |
| updatedAt | string | ISO timestamp record last updated |
Card Card (Bonuskarte)
| Feld | Typ | Beschreibung |
|---|
| id | string | nanoid(16) primary key |
| barbershopId | string | Tenant scope |
| customerId | string? | Owner of the card (optional until activation) |
| cardCode | string | Unique card code printed on the physical card |
| status | enum | NOT_ACTIVATED | ACTIVE | BLOCKED | LOST | EXPIRED |
| pointsBalance | number | Current points balance |
| totalPointsEarned | number | Lifetime points earned |
| totalPointsRedeemed | number | Lifetime points redeemed |
| activatedAt | string? | ISO timestamp of activation |
| blockedAt | string? | ISO timestamp the card was blocked |
| expiresAt | string? | ISO timestamp the card expires |
| replacedByCardId | string? | If replaced, points to the new card.id |
| createdAt | string | ISO timestamp record created |
| updatedAt | string | ISO timestamp record last updated |
Transaction Transaction (Punkte-Transaktion)
| Feld | Typ | Beschreibung |
|---|
| id | string | nanoid(16) primary key |
| barbershopId | string | Tenant scope |
| cardId | string | Card the points moved on |
| customerId | string? | Customer reference (denormalized for queries) |
| userId | string? | Staff/owner who performed the action |
| type | enum | EARN | REDEEM | MANUAL_ADJUSTMENT | EXPIRE | CANCEL |
| points | number | Signed points delta (positive = earn, negative = redeem) |
| balanceBefore | number | Card balance before this transaction |
| balanceAfter | number | Card balance after this transaction |
| amountEuro | number? | Euro amount of the underlying purchase (service price, when earned via a catalogue service) |
| reason | string | Human-readable reason (service name, manual reason, etc.) |
| rewardId | string? | Reward reference when type=REDEEM |
| metadata | object? | Arbitrary key-value metadata for future fields |
| createdAt | string | ISO timestamp of the transaction |
Service Service (Dienstleistung)
| Feld | Typ | Beschreibung |
|---|
| id | string | nanoid(16) primary key |
| barbershopId | string | Tenant scope |
| categoryId | string | Foreign key to ServiceCategory.id |
| name | string | Service name as shown to staff |
| description | string? | Service description (optional) |
| priceEur | number | Base price in EUR |
| pointsAward | number | Points granted when this service is delivered |
| durationMin | number? | Approximate duration in minutes (optional) |
| sortIndex | number | Display ordering within category |
| active | boolean | True if currently bookable / shown to staff |
| createdAt | string | ISO timestamp record created |
| updatedAt | string | ISO timestamp record last updated |
ServiceCategory ServiceCategory (Leistungskategorie)
| Feld | Typ | Beschreibung |
|---|
| id | string | nanoid(16) primary key |
| barbershopId | string | Tenant scope |
| name | string | Category name as shown to staff |
| color | string? | Hex accent colour for the UI (optional) |
| sortIndex | number | Display ordering of the category |
| active | boolean | True if currently shown to staff |
| createdAt | string | ISO timestamp record created |
| updatedAt | string | ISO timestamp record last updated |
Reward Reward (Prämie)
| Feld | Typ | Beschreibung |
|---|
| id | string | nanoid(16) primary key |
| barbershopId | string | Tenant scope |
| categoryId | string? | Optional reward category reference |
| title | string | Reward title shown to customers |
| description | string? | Reward description (optional) |
| pointsCost | number | Points required to redeem this reward |
| type | enum | DISCOUNT | FREE_SERVICE | GIFT | CUSTOM |
| discountAmount | number? | Fixed discount in EUR for DISCOUNT type |
| discountPercent | number? | Percentage discount for DISCOUNT type |
| active | boolean | True if currently available for redemption |
| validFrom | string? | ISO date — reward available from this date |
| validUntil | string? | ISO date — reward available until this date |
| maxRedemptions | number? | Hard cap on total redemptions (optional) |
| iconName | string? | Curated lucide-react icon key |
| createdAt | string | ISO timestamp record created |
| updatedAt | string | ISO timestamp record last updated |
Redemption Redemption (Prämien-Einlösung)
| Feld | Typ | Beschreibung |
|---|
| id | string | nanoid(16) primary key |
| barbershopId | string | Tenant scope |
| rewardId | string | Reward that was redeemed |
| cardId | string | Card the points came off |
| customerId | string | Customer who redeemed |
| userId | string? | Staff/owner who confirmed the redemption |
| pointsSpent | number | Points debited for this redemption |
| status | enum | COMPLETED | CANCELLED |
| cancelledAt | string? | ISO timestamp if status=CANCELLED |
| createdAt | string | ISO timestamp of the redemption |
Coupon Coupon (Gutschein-Aktion)
| Feld | Typ | Beschreibung |
|---|
| id | string | nanoid(16) primary key |
| barbershopId | string | Tenant scope |
| title | string | Coupon title shown to staff and customer |
| description | string? | Coupon description (optional) |
| kind | enum | BONUS_POINTS | DISCOUNT_NOTE |
| bonusPoints | number? | Points credited for kind=BONUS_POINTS |
| discountLabel | string? | Free-text discount note for kind=DISCOUNT_NOTE |
| validFrom | string? | ISO date — coupon valid from |
| validUntil | string? | ISO date — coupon valid until |
| maxRedemptions | number? | Hard cap on total redemptions (optional) |
| perCustomerLimit | number | How often one customer may use it |
| active | boolean | True if currently applicable |
| createdBy | string | User id that created the coupon |
| createdAt | string | ISO timestamp record created |
| updatedAt | string | ISO timestamp record last updated |
CouponRedemption CouponRedemption (Gutschein-Einlösung)
| Feld | Typ | Beschreibung |
|---|
| id | string | nanoid(16) primary key |
| barbershopId | string | Tenant scope |
| couponId | string | Coupon that was applied |
| cardId | string | Card the coupon was applied to |
| customerId | string? | Customer holding the card (optional) |
| userId | string | Cashier (User id) who applied it |
| pointsAwarded | number | Points credited (0 for DISCOUNT_NOTE) |
| createdAt | string | ISO timestamp of the redemption |
Barbershop Barbershop (Tenant / Geschäftseinheit)
| Feld | Typ | Beschreibung |
|---|
| id | string | nanoid(16) primary key |
| name | string | Business display name |
| legalName | string? | Full legal company name (optional) |
| ownerName | string? | Contact person (optional) |
| address | string | Street and house number |
| postalCode | string | Postal code |
| city | string | City |
| country | string | ISO country code, default DE |
| phone | string | Business phone number |
| email | string | Business contact email |
| website | string? | Business website URL (optional) |
| logoUrl | string? | Logo image URL (R2/CDN) |
| primaryColor | string? | Brand color in hex (default #C9A04A) |
| currency | string | ISO 4217 currency code (default EUR) |
| timezone | string | IANA timezone (default Europe/Berlin) |
| programRules | string? | Public program rules / terms of participation |
| birthdayMessage | string? | Birthday greeting email body |
| status | enum | ACTIVE | SUSPENDED | ARCHIVED | SCHEDULED_FOR_DELETION |
| cardDesign | object? | Two-sided card design (front + back + QR placement) |
| betaSignup | boolean? | True if the business signed up during the beta (AGB §3a customer floor) |
| planId | enum? | free | starter | pro | business | enterprise (undefined = free) |
| limitOverrides | object? | Per-tenant plan limit overrides |
| businessHours | array? | Weekly opening hours per weekday |
| instagramUrl | string? | Instagram profile URL |
| facebookUrl | string? | Facebook profile URL |
| googleMapsUrl | string? | Google Maps profile URL |
| descriptionDe | string? | Public profile description (German, max 800 chars) |
| descriptionEn | string? | Public profile description (English, max 800 chars) |
| descriptionUk | string? | Public profile description (Ukrainian, max 800 chars) |
| descriptionRu | string? | Public profile description (Russian, max 800 chars) |
| emailSignature | string? | Appended to outgoing marketing/birthday emails (max 500 chars) |
| autoAnonymizeMonths | number? | GDPR Art. 5 retention threshold in months (0 = disabled) |
| autoAnonymizeNotifyDays | string? | Comma-separated days before anonymize (e.g. '30,14,7') |
| customGdprConsentDe | string? | Per-tenant override of GDPR consent text (German) |
| customGdprConsentEn | string? | Per-tenant override of GDPR consent text (English) |
| customGdprConsentUk | string? | Per-tenant override of GDPR consent text (Ukrainian) |
| customGdprConsentRu | string? | Per-tenant override of GDPR consent text (Russian) |
| avvAcceptedAt | string? | ISO timestamp of AVV (DPA) acceptance |
| avvAcceptedIp | string? | IP address captured at AVV acceptance |
| avvAcceptedUserAgent | string? | User agent captured at AVV acceptance |
| avvVersion | string? | Version of AVV accepted |
| policyAcceptances | array? | Append-only log of AVV/AUP/AGB/Widerruf/data-act-switching acceptances |
| createdAt | string | ISO timestamp record created |
| updatedAt | string | ISO timestamp record last updated |
2. Export-Formate
Alle Exporte werden als JSON gemäß RFC 8259, UTF-8-kodiert, mit 2-Leerzeichen-Einrückung ausgegeben. Schema-Versionen: boncard.tenant.v1 (Tenant-Export, gesamtes Geschäft) und boncard.customer.v1 (End-Kunden-Export, Einzeldaten). Datumsfelder sind ISO-8601-Strings; optionale Felder sind weggelassen statt null.
3. Interoperabilität
Punkte-Berechnungslogik: Punkte werden über den Leistungskatalog vergeben — jede Leistung definiert einen festen pointsAward, der beim Erfassen durch das Personal gutgeschrieben wird; manuelle Korrekturen (mit dokumentiertem Grund) sind möglich. AVV-Pflichten: Vor Vertragsabschluss wird ein elektronischer Auftragsverarbeitungsvertrag (Art. 28 DSGVO) abgeschlossen; alle Personaldaten werden bcrypt-gehasht, Audit-Logs 90 Tage gespeichert. Aufbewahrungsfristen: Marketing-E-Mail-Events 90 Tage, Transaktionale 1 Jahr, Legal-Notices 3 Jahre; Einwilligungs-Logs 3 Jahre nach Widerruf; Löschprotokolle 3 Jahre.
Internationaler Zugriff & IKT-Infrastruktur (Art. 28(1) Data Act)
Gemäß Art. 28(1) Verordnung (EU) 2023/2854 (Data Act) legt Boncard offen, in welcher Jurisdiktion die für den Dienst eingesetzte IKT-Infrastruktur betrieben wird (lit. a) und welche Maßnahmen gegen rechtswidrigen behördlichen Zugriff aus Drittländern auf nicht-personenbezogene Daten bestehen (lit. b).
(a) Jurisdiktion der IKT-Infrastruktur
- Boncard selbst: Betrieb durch Einzelunternehmer in Deutschland (EU)
- Railway Corp. (Application Hosting): EU (Amsterdam, europe-west4)
- MongoDB Atlas (Datenbank-Speicherung): EU (Frankfurt am Main, Deutschland)
- Resend Inc. (Transaktionale E-Mails): USA (AWS)
- Google LLC (Google Wallet API) (Wallet-Karten (Google Wallet)): USA
- Cloudflare, Inc. (R2) (Objektspeicher (Logos und Datenbank-Backups)): EU (jurisdictional bucket, R2_JURISDICTION=eu)
(b) Maßnahmen gegen rechtswidrigen behördlichen Drittlandszugriff auf nicht-personenbezogene Daten
- Verschlüsselung bei Übertragung (TLS 1.2+) und im Ruhezustand (AES-256)
- Zugriffskontrollen / Least-Privilege-Prinzip
- Vertragliche Garantien (EU-Standardvertragsklauseln 2021/914 bzw. DPF-Zertifizierungen der Anbieter)
- Transparenz- und Anfechtungsrichtlinien der Anbieter gegenüber Behördenanfragen
- Datenminimierung (persistente Daten primär in MongoDB Atlas)
Diese Angaben erfüllen Art. 28(1)(a)/(b) Data Act. Die jeweils aktuelle Subprozessoren-Liste: /subprocessors.
4. Versionierungs-Richtlinie
Schema-Bumps sind entweder rückwärtskompatibel (zusätzliche optionale Felder) oder werden mit 90 Tagen Vorlauf angekündigt. Änderungen werden den aktiven Tenants per LEGAL_NOTICE-E-Mail mitgeteilt. Die aktuelle Schema-Version steht in jeder Export-Datei im Feld schemaVersion.