Stack & Setup
- Dockerisierte Dev-Umgebung (PHP 8.4-FPM, Nginx, MariaDB 11.4)
- Symfony 7.4 + API Platform 4.3, Doctrine ORM, LexikJWT, Messenger
- Vue 3 + TS (Vite), Vue Router, Pinia, Axios
Kern-Domäne & Auth
- Entitäten: User, PlatformPlan, Reseller, Company, Domain, Location,
Employee, ContactLink (UUIDv7)
- JWT-Login (/api/login), Rollen-Hierarchie, /api/me
- Mandantentrennung via API-Platform-Query-Extension (Lesen) +
TenantStampProcessor (Schreiben)
Öffentliche Profile (SSR)
- Profil-Landingpage, vCard-Download, QR-Code im Marken-Look
- Stabiler NFC/QR-Kurz-Link /t/{code} -> Redirect aufs aktuelle Profil
- Firmenspezifisches Branding (Farben/Logo) auf der Profilseite
Verwaltungsoberfläche (SPA)
- Brand-Look (dunkle Sidebar), rollenbasierte Navigation
- Dashboard, Reseller (+Provisioning), Firmen, Mitarbeiter, Standorte,
Domains, Design/Branding mit Live-Vorschau
Konzept & Doku: docs/KONZEPT.md (inkl. Wallet/Sync §12), README.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
18 KiB
vcard4reseller — Konzept & Datenmodell
White-Label-Plattform für digitale Visitenkarten (PrintShopCreator). Reseller (Druckereien/Agenturen) bieten ihren Firmenkunden ein zentrales Identity-Management an: ein Mitarbeiterprofil als Single Source of Truth, das in alle digitalen und gedruckten Ausgabekanäle synchronisiert wird.
Status: Konzeptphase — noch kein Code. Stack-Entscheidung: Symfony (API Platform, JSON-API) + eigenständige Vue 3 SPA (Vite), MariaDB. Architektur Variante A (SPA + getrennte API).
1. Ziele & Leitprinzipien
- Single Source of Truth: Eine Änderung am Mitarbeiterprofil propagiert in alle Kanäle (vCard, Wallet, Landingpage, NFC, QR, Druckdaten, E-Mail-Signatur).
- Mandantenfähigkeit (Multi-Tenancy): Strikte Datentrennung pro Reseller und pro Firmenkunde.
- White-Label: Jeder Reseller / Firmenkunde tritt unter eigener Domain & eigenem Branding auf.
- Wiederkehrender Umsatz: Reseller zahlt Plattformgebühr (Paket), Firmenkunde zahlt Reseller (eigene Preisgestaltung).
- Öffentliche Profilseiten müssen schnell & SEO-fähig sein → serverseitig gerendert (Twig), getrennt von den Dashboards (Vue-SPA).
2. Rollen & Berechtigungen
| Rolle | Symfony-Role | Scope | Kann |
|---|---|---|---|
| Plattform-Admin | ROLE_PLATFORM_ADMIN |
global | Reseller anlegen/verwalten, Plattform-Pakete, Abrechnung gegenüber Resellern, Demo-Leads |
| Reseller-Admin | ROLE_RESELLER_ADMIN |
1 Reseller | Firmenkunden + Domains + Branding anlegen, Preise definieren, Druckaufträge, Reseller-Mitarbeiter |
| Firmen-Admin | ROLE_COMPANY_ADMIN |
1 Company | Standorte, Mitarbeiter/Profile, Templates der Firma, NFC/QR ausrollen |
| Mitarbeiter | ROLE_EMPLOYEE |
1 Profil | eigenes Profil pflegen (sofern Firma das erlaubt) |
| Öffentlicher Besucher | — | — | Profilseite ansehen, vCard/Wallet herunterladen, NFC/QR scannen |
Hierarchie der Mandanten: Plattform → Reseller → Company → Location → Employee/Profil
Durchsetzung über:
- Doctrine-Filter (automatisches Scoping nach
reseller_id/company_idje nach eingeloggtem Kontext) - Security Voters (z. B.
ProfileVoter,CompanyVoter) für feingranulare Aktionen - API-Platform
security-Attribute pro Resource & Operation
3. Architektur
┌──────────────────────────┐ ┌──────────────────────────────────┐
│ Vue 3 SPA (Vite) │ │ Öffentliche Profilseiten │
│ - Reseller-Dashboard │ │ (Symfony + Twig, SSR, SEO) │
│ - Firmen-Dashboard │ │ /{slug}, /c/{company}/{slug} │
│ - Mitarbeiter-Self-Svc │ │ vCard-Download, Wallet, QR-Ziel │
└───────────┬──────────────┘ └─────────────────┬────────────────┘
│ JSON (JWT/Session) │
▼ ▼
┌──────────────────────────────────────────────────────────────────────┐
│ Symfony Backend │
│ ├─ API Platform (REST/JSON, OpenAPI) │
│ ├─ Security (JWT/LexikJWT od. Session), Voters, Doctrine-Filter │
│ ├─ Domain Services (ProfileSync, ChannelGenerator) │
│ ├─ Messenger (async): Wallet-Pass, Druckdaten-PDF, QR/NFC, E-Mail │
│ └─ Doctrine ORM │
└───────────┬───────────────────────┬───────────────────┬──────────────┘
▼ ▼ ▼
MariaDB Object-Storage externe Dienste
(Mandantendaten) (Assets, PDFs, Pässe) (Apple/Google Wallet,
Payment/Stripe, Mail)
Generierungs-Pipeline (ChannelGenerator): Bei jeder Profiländerung wird ein
ProfileUpdated-Event ausgelöst → Messenger-Handler regenerieren die abgeleiteten
Artefakte (vCard-String, Wallet-Pass, QR/Kurz-URL, Druck-PDF, E-Mail-Signatur-HTML)
und legen sie versioniert ab. Öffentliche Endpunkte liefern immer den aktuellen Stand.
4. Datenmodell (Entitäten)
Kernentitäten
- User — Auth-Identität. Felder:
email,password,roles[],status,lastLogin. Verknüpft optional mitreseller_id,company_id,employee_id(je nach Rolle). - Reseller — oberster Mandant. Felder:
name,slug,primaryDomain, Branding-Defaults,status,platformPlan_id. Hat Limits aus dem Plattform-Paket. - PlatformPlan — Reseller-Pakete (Starter/Professional/Business). Felder:
name,pricePerMonth,maxProfiles,maxCompanies,features[]. - ResellerSubscription — Abo des Resellers bei der Plattform. Felder:
plan_id,status,startedAt,renewsAt,paymentRef(Stripe). - Company (Firmenkunde) — gehört zu Reseller. Felder:
name,slug, Branding (Logo, Farben, Fonts),defaultLocation_id,status,selfEditEnabled(erlaubt Mitarbeiter-Self-Service generell). Domains ausgelagert inDomain. - Domain — gehört zu Company. Felder:
hostname,type(subdomain|custom),status(pending|verified|failed),verificationCheckedAt,tlsStatus. Custom-Domains zeigen per A-Record auf unsere IP (siehe §11). - Location (Standort) — gehört zu Company. Felder: Adresse, Geo, Telefon, Öffnungszeiten, Standort-spezifisches Branding-Override.
- Employee / Profile — gehört zu Company, optional Location. Single Source of Truth. Felder: Name, Titel, Position, Abteilung, Telefon(e), E-Mail, Foto, Bio,
slug(öffentlich),status,user_id(optional, für Self-Service),selfEditAllowed(bool — Firmen-Admin gibt diesen Mitarbeiter frei),editableFields(optional: welche Felder der Mitarbeiter ändern darf). - ContactLink — Social-/Web-Links eines Profils (Typ + URL + Reihenfolge). 1:n zu Employee.
Branding & Vorlagen
- Template — Vorlage, scoped auf Reseller oder Company. Felder:
type(print_card|email_signature|landing_page|wallet_pass),name,config(JSON),assets. - Asset — hochgeladene Datei (Logo, Foto, Hintergrund). Felder:
path,mimeType,owner(polymorph: reseller/company/employee).
Ausgabekanäle (generierte Artefakte)
- NfcTag — physischer Tag. Felder:
uid,shortUrl,employee_id(nullable bis Zuweisung),status(unassigned/active/disabled),lastScanAt. - QrCode — generierter QR (statisch oder dynamische Redirect-URL). Felder:
target,imageAsset,employee_id. - WalletPass — Referenz auf Apple/Google-Pass. Felder:
provider(apple|google),serial,authToken(Apple Web-Service-Auth),passUrl,employee_id,lastGeneratedAt. Siehe §12. - WalletDevice — registriertes Apple-Gerät für Push-Updates (Apple-spezifisch). Felder:
deviceLibraryId,pushToken,serial(→ WalletPass),registeredAt. Google braucht das nicht (Server-Push über die API). - GeneratedArtifact (optional, generisch) — Cache abgeleiteter Outputs:
type(vcard/print_pdf/signature_html),employee_id,payload/fileRef,generatedAt,version.
Vertrieb & Abrechnung
- Invoice — Rechnung Plattform→Reseller. Felder:
reseller_id,amount,period,status,pdfRef. (Reseller→Company-Rechnungen vorerst nicht Teil der Plattform, siehe §8.) CompanySubscription / PricePlan— zurückgestellt: Reseller↔Company-Abrechnung läuft außerhalb der Plattform.- PrintOrder — Druckauftrag. Felder:
company_id,status(new/in_production/shipped),items[](je Employee),printDataRef.
Marketing / Sonstiges
- DemoRequest — Lead vom Marketing-Formular (
/demo). Felder: Name, Firma, E-Mail, Telefon, Nachricht,status. - AuditLog — Nachvollziehbarkeit kritischer Aktionen (wer/was/wann).
5. ER-Diagramm
erDiagram
PlatformPlan ||--o{ Reseller : "definiert Limits"
Reseller ||--|| ResellerSubscription : hat
Reseller ||--o{ Company : verwaltet
Reseller ||--o{ User : "Reseller-Staff"
Reseller ||--o{ Template : "Defaults"
Company ||--o{ Location : hat
Company ||--o{ Domain : "Sub-/Custom-Domain"
Company ||--o{ Employee : beschäftigt
Company ||--o{ User : "Firmen-Admins"
Company ||--o{ Template : "eigene"
Location ||--o{ Employee : "zugeordnet"
Employee ||--o{ ContactLink : hat
Employee ||--o| NfcTag : verknüpft
Employee ||--o{ QrCode : hat
Employee ||--o| WalletPass : hat
WalletPass ||--o{ WalletDevice : "Apple-Push"
Employee ||--o{ GeneratedArtifact : erzeugt
Employee ||--o| User : "Self-Service"
Company ||--o{ PrintOrder : bestellt
Reseller ||--o{ Invoice : "Plattform→Reseller"
6. API-Struktur (API Platform, grob)
Alle unter /api, JWT-geschützt, mandantengescoped. Beispiele:
| Resource | Operationen | Zugriff |
|---|---|---|
/api/resellers |
CRUD | Platform-Admin |
/api/companies |
CRUD | Reseller-Admin (eigene), Platform-Admin |
/api/locations |
CRUD | Company-/Reseller-Admin |
/api/employees |
CRUD | Company-Admin; GET self für Employee |
/api/employees/{id}/regenerate |
POST | löst Kanal-Neugenerierung aus |
/api/templates |
CRUD | Reseller-/Company-Admin |
/api/nfc-tags |
CRUD + assign | Company-Admin |
/api/print-orders |
CRUD | Company-Admin / Reseller |
/api/demo-requests |
POST (öffentlich), GET (Admin) | Marketing |
Öffentliche, nicht-API Endpunkte (Symfony/Twig, SSR):
GET /{slug}oderhttps://{companyDomain}/{employeeSlug}→ LandingpageGET /{slug}/vcard.vcf→ vCard-DownloadGET /{slug}/wallet→ Wallet-PassGET /t/{nfcShortCode}→ NFC/QR-Redirect auf aktuelles Profil
7. Sicherheit & Mandantentrennung
- Auth: LexikJWTAuthenticationBundle (Stateless API) oder Session für SPA-Komfort. Refresh-Tokens.
- Scoping: Doctrine-
Filter, der je nach eingeloggtem User automatischreseller_id/company_idin alle Queries injiziert. Kein Datenleck zwischen Mandanten. - Voters:
CompanyVoter,EmployeeVoter,TemplateVoterfür Aktion×Objekt-Prüfung. - Domain-Auflösung: Middleware ermittelt aus Host-Header (Custom-Domain/Subdomain) den Mandantenkontext für öffentliche Seiten.
- Datenschutz: DSGVO — Mitarbeiterdaten, Lösch-/Export-Funktion, AuditLog. Standort EU.
8. Billing-Modell
Entscheidung: Vorerst rechnet nur die Plattform gegenüber dem Reseller ab. Wie der Reseller seinerseits mit den Firmenkunden abrechnet, regelt er selbst — das ist nicht Teil der Plattform (Stand jetzt). Eine spätere optionale Reseller→Company-Fakturierung bleibt als Erweiterung vorgemerkt.
- Plattform → Reseller (umgesetzt): Reseller wählt
PlatformPlan(Starter 99 € / Professional 249 € / Business 599 €). Stripe-Abo, Limits (maxProfiles,maxCompanies) werden durchgesetzt. - Reseller → Company (außerhalb der Plattform): Reseller vereinbart Preise selbst. Plattform bietet höchstens einen Umsatzrechner/Reporting zur Orientierung, aber keine Rechnungsstellung. → Entität
CompanySubscription/Invoice(Reseller→Company) wird zurückgestellt auf Phase „später, falls gewünscht". - Umsatzrechner (aus Marketing): Profile × Preis − Plattformgebühr = Marge (rein informativ).
9. Roadmap / Phasen
Phase 0 — Setup (nach Konzept-Freigabe)
- Symfony-Projekt + API Platform + Doctrine + MariaDB
- Vue 3 SPA (Vite) Grundgerüst, Auth-Flow
- CI, Docker-Dev-Umgebung
Phase 1 — Kern-Domäne & Auth
- Entitäten: User, Reseller, Company, Location, Employee, ContactLink
- Rollen, Voters, Doctrine-Mandantenfilter
- Reseller-/Firmen-Dashboards (CRUD)
Phase 2 — Öffentliche Profile
- SSR-Landingpage, vCard-Download, QR-Code, Kurz-URL-Redirect
- Branding/Template-Engine (landing_page)
Phase 3 — Ausgabekanäle
- Wallet-Pässe (Apple/Google), E-Mail-Signaturen, Druckdaten-PDF
- NFC-Tag-Verwaltung & Zuweisung
- Messenger-Pipeline für async Generierung
Phase 4 — Billing & Vertrieb
- Plattform-Pakete + Stripe, Limit-Durchsetzung
- Reseller-Preisgestaltung, Umsatzrechner, PrintOrders, Invoices
Phase 5 — Marketing-Site & Politur
- Öffentliche Landingpage (vcard4reseller.de-Nachbau), Demo-Formular → DemoRequest
- Reporting, AuditLog, DSGVO-Tools
10. Geklärte Entscheidungen & offene Fragen
Geklärt (2026-05-30)
- Domains: Standard = Subdomains (
firma.reseller.de). Firmenkunden können zusätzlich eine eigene Domain hinterlegen; sie müssen dann selbst per A-Record auf unsere IP zeigen. Plattform übernimmt Domain-Verifikation + TLS-Provisionierung (Let's Encrypt). Siehe §11. - Self-Service: Möglich, aber muss vom Firmen-Admin freigegeben werden — zweistufig:
Company.selfEditEnabled(Firma erlaubt es generell) +Employee.selfEditAllowed(pro Mitarbeiter). Mitarbeiter bearbeitet nur freigegebene Felder. - Billing: Nur Plattform → Reseller (siehe §8). Reseller↔Company außerhalb der Plattform; spätere Erweiterung möglich.
Noch offen
- Wallet-Pässe: Apple Developer Account + Google Wallet API vorhanden? (Zertifikate erforderlich). White-Label-Frage: läuft der Apple-Pass unter einem zentralen Pass Type ID (Plattform) oder pro Reseller? Konzept dazu in §12.
- Druckdaten: Welches Format erwarten die Druckereien (PDF/X, bestimmte Maße, Beschnitt)? Gibt es Vorlagen?
- Bestehende Daten/Branding: Existieren Design-Assets/CI zur bestehenden vcard4reseller.de, die wir übernehmen?
11. Domain-Handling
Subdomain (Default): Jede Company bekommt {company.slug}.{reseller.primaryDomain}. Wildcard-DNS + Wildcard-TLS auf Reseller-Ebene → keine Aktion durch den Kunden nötig.
Eigene Domain (optional):
- Firmen-Admin trägt seine Domain (
visitenkarte.firma.de) im Dashboard ein → Statuspending. - Plattform zeigt die einzutragende IP (A-Record) (+ optional CNAME) an.
- Kunde setzt den DNS-Eintrag bei seinem Provider.
- Plattform prüft periodisch (DNS-Lookup) → bei Treffer Status
verified, dann automatisches TLS-Zertifikat (Let's Encrypt / ACME). - Reverse-Proxy/Router löst den
Host-Header → Mandantenkontext (Company) auf.
Datenmodell-Ergänzung an Company bzw. neue Entität Domain:
hostname, type (subdomain|custom), status (pending|verified|failed), verificationCheckedAt, tlsStatus, company_id.
12. Wallet-Pässe & Kontakt-Synchronisation
Das Grundproblem
Eine heruntergeladene vCard (.vcf) ist ein Schnappschuss: Einmal in der Kontakte-App gespeichert, aktualisiert sie sich nicht mehr. Ändert ein Mitarbeiter seine Nummer, haben alle Empfänger veraltete Daten. Das ist eine OS-Einschränkung. Es gibt drei Wege zu „aktuellen Daten":
| Weg | Echter Auto-Sync? | Wo landet es? | Aufwand |
|---|---|---|---|
| Wallet-Pass (Apple/Google) | Ja, over-the-air | Wallet-App (nicht Kontakte) | hoch (Zertifikate) |
| Link behalten (QR/NFC/Kurz-URL) | Daten aktuell beim Öffnen, vCard wird live generiert | Browser → Kontakt bei Bedarf neu | ✅ umgesetzt (Phase 2) |
| CardDAV | Ja, auf Kontakt-Ebene | native Kontakte-App | mittel, für Einzel-Empfänger unpraktisch (account-basiert) |
Fazit: Den einzigen echten „Push in eine bereits gespeicherte Karte" liefert der Wallet-Pass. Der Link (unsere Profilseite) ist der pragmatische Standard: nicht die Daten, sondern der Link wird gespeichert; die vCard erzeugen wir bei jedem Abruf frisch.
Apple Wallet (PassKit)
.pkpass= ZIP auspass.json+ Bildern +manifest.json+ PKCS#7-Signatur (Pass Type ID-Zertifikat + Apple-WWDR-Cert).- Pass-Stil
generic(Visitenkarte): Name / Position / Firma / Telefon / E-Mail / Link. - PHP: z. B.
includable/php-pkpass, gekapselt in einemWalletService. - Sync (Push): Pass enthält
webServiceURL+authenticationToken.- Nutzer fügt Pass hinzu → Gerät registriert sich (→
WalletDevice). - Profil ändert sich → leerer APNs-Push an registrierte Geräte.
- Gerät holt aktualisierten Pass von unserer PassKit-Web-Service-API ab.
- Nutzer fügt Pass hinzu → Gerät registriert sich (→
- Nötige Endpunkte (PassKit Web Service):
register,unregister,list serials,latest pass.
Google Wallet
- REST-API + Service-Account (kostenlos). Generic-Pass-Klasse + pro Mitarbeiter ein Objekt.
- Hinzufügen via „Add to Google Wallet"-Link (signiertes JWT).
- Sync: Objekt server-seitig per API patchen → Google pusht selbst. Kein APNs, keine Geräte-Registrierung. → einfacher als Apple, daher als erster Wallet-Kanal empfohlen.
Einbettung in die Architektur
Employee geändert ──► ProfileUpdated-Event ──► Messenger (async)
├─ Apple: Pass neu signieren + APNs-Push an WalletDevices
└─ Google: Wallet-Objekt per API patchen (Google pusht selbst)
Neue Bausteine: WalletPass/WalletDevice (Datenmodell §4), WalletService (Pass bauen/signieren/patchen), ApplePassController (Web-Service-Endpunkte), Messenger-Handler am ProfileUpdated-Event (gleicher Trigger wie Employee::touch()).
White-Label-Überlegung
Apple Pass Type ID ist an einen Apple-Account gebunden. Optionen: (a) ein zentraler Pass Type ID der Plattform für alle Reseller (einfacher, Branding über Pass-Felder/Logo) oder (b) pro Reseller ein eigener (aufwändig, jeder Reseller braucht Apple-Account). Empfehlung: (a) zentral, Reseller-/Firmen-Branding über Logo & Farben im Pass. → zu klären (offene Frage #4).
Voraussetzungen (offen)
- Apple Developer Account + Pass Type ID + Zertifikat (
.p12) — für Apple Wallet. - Google-Cloud-Projekt + Wallet-API + Service-Account — für Google Wallet.