chore: initial Nimuli design spec
commit
364d1448c7
|
|
@ -0,0 +1,789 @@
|
|||
# Nimuli — Master Design Spec
|
||||
|
||||
**Datum:** 2026-05-15
|
||||
**Status:** Design abgeschlossen, bereit für Implementation-Planung
|
||||
**Autor:** Boban Blaskovic
|
||||
**Projekt-Typ:** Standalone SaaS (eigene Codebase, getrennt von CluPilot)
|
||||
|
||||
---
|
||||
|
||||
## 1. Produkt-Übersicht
|
||||
|
||||
**Nimuli** ist eine All-in-One Link-Management-Plattform.
|
||||
|
||||
**Kern-Features:**
|
||||
- Short-Links mit Whitelabel-Custom-Domains
|
||||
- Dynamische QR-Codes (Logo, Farben, alle Types: URL/vCard/WiFi/PDF/MP3/Text)
|
||||
- Link-in-Bio Microsites mit Block-Editor
|
||||
- Smart-Routing (Geo, Device, OS, Time, Language, UTM)
|
||||
- A/B-Testing mit Conversion-Tracking
|
||||
- Pixel-Injection (Meta/Google/TikTok via Interstitial)
|
||||
- Klick-Analytics mit Live-Dashboards
|
||||
- AI-Suite (Auto-Slug, A/B-Generator, Anomalie-Detection, Insight-Reports, Routing-per-Prompt)
|
||||
- REST-API + Outgoing-Webhooks
|
||||
- Multi-Workspace (Agency-Use-Case)
|
||||
|
||||
**Markt:** DACH-Start, DE+EN i18n, geplant EU-Expansion.
|
||||
|
||||
**Marken + Domains:**
|
||||
- Marke: **Nimuli**
|
||||
- Marketing-Site: `nimuli.com`
|
||||
- App/Dashboard: `app.nimuli.com`
|
||||
- Short-Redirect-Domain: `nimu.li`
|
||||
- Whitelabel-Subdomains: `*.nimu.li` (User-Subdomain)
|
||||
- Custom-Domains: `{kunden-domain}.com` via CNAME → nimu.li
|
||||
|
||||
**Differenzierung:**
|
||||
- Pixel-Injection im Standard-Plan (Bitly ignoriert das)
|
||||
- AI-Suite ab Tag 1 (Konkurrenz hat das nicht oder spärlich)
|
||||
- Agency-fokussierte Features (Multi-Workspace, Whitelabel, Reseller)
|
||||
- EU-Hosting, DSGVO-first
|
||||
|
||||
---
|
||||
|
||||
## 2. Gesamt-Architektur
|
||||
|
||||
### Lokale Entwicklung
|
||||
Single-Host Docker Compose. Alle Services in einem Compose-File:
|
||||
- `nginx`, `app` (Octane), `queue`, `scheduler`, `reverb`, `mysql`, `redis`, `mailpit`
|
||||
|
||||
### Production-Cluster (Hetzner Cloud)
|
||||
|
||||
```
|
||||
┌──────────────────────┐
|
||||
│ Hetzner LB (Public) │
|
||||
│ Failover-IP, SSL │
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
┌──────────────────────┼──────────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌────────┐ ┌────────┐ ┌────────┐
|
||||
│ web-01 │ │ web-02 │ │ web-03 │
|
||||
│ Docker │ │ Docker │ │ Docker │
|
||||
│ app+ │ │ app+ │ │ app+ │
|
||||
│ nginx │ │ nginx │ │ nginx │
|
||||
└───┬────┘ └───┬────┘ └───┬────┘
|
||||
│ │ │
|
||||
└──────────┬──────────┴──────────┬──────────┘
|
||||
│ │
|
||||
┌─────▼─────┐ ┌─────▼─────┐
|
||||
│ DB-01 │ │ Redis-01 │
|
||||
│ MySQL │ │ Cache+ │
|
||||
│ │ │ Queue+ │
|
||||
│ │ │ Sessions │
|
||||
└───────────┘ └───────────┘
|
||||
|
||||
┌──────────────┐ ┌──────────────────┐
|
||||
│ worker-01 │ │ CDN (bunny.net │
|
||||
│ Queue + │ │ oder Hetzner │
|
||||
│ Scheduler + │ │ Storage Box CDN) │
|
||||
│ Reverb │ │ │
|
||||
└──────────────┘ └──────────────────┘
|
||||
```
|
||||
|
||||
**DNS-Routing:**
|
||||
- `app.nimuli.com` → LB-IP → web-Pool
|
||||
- `nimu.li`, `*.nimu.li` → LB-IP → web-Pool (Redirect-Service)
|
||||
- `ws.nimuli.com` → direkt worker-01 (Reverb, kein LB)
|
||||
- `{custom-domain}` → CNAME → nimu.li → LB
|
||||
|
||||
**Kosten:** ~60-85€/Monat Hetzner.
|
||||
|
||||
### Drei Haupt-Subsysteme
|
||||
|
||||
1. **Redirect-Service** — Hot-Path. `nimu.li/{slug}` → Redis-Lookup → 302 + async Klick-Event. Ziel <10ms p95.
|
||||
2. **App-Dashboard** — `app.nimuli.com`. Livewire-CRUD, Analytics, Workspace-Mgmt, Billing.
|
||||
3. **Background-Worker** — Queue für Klick-Events → Rollups, Email, Stripe-Webhooks, AI-Calls, Pixel-Server-Side-Sends, Domain-SSL-Provisioning.
|
||||
|
||||
---
|
||||
|
||||
## 3. Stack
|
||||
|
||||
| Bereich | Technologie |
|
||||
|---------|-------------|
|
||||
| Backend | Laravel 12 + Octane (Swoole oder FrankenPHP) |
|
||||
| Frontend | Livewire v3 + Alpine.js + Tailwind v4 (`@theme`) |
|
||||
| Realtime | Laravel Reverb (WebSockets) |
|
||||
| Auth | Laravel Breeze (Livewire-Stack) + `pragmarx/google2fa` (2FA) |
|
||||
| Permissions | `spatie/laravel-permission` (per Workspace scoped) |
|
||||
| Modals | Wire Elements Modal |
|
||||
| Charts | Chart.js 4.x |
|
||||
| QR-Codes | `chillerlan/php-qrcode` (Backend), QR-JS-Lib für Live-Preview |
|
||||
| Image-Processing | `intervention/image-laravel` (Resize, WebP, EXIF-Strip, Auto-Rotate) |
|
||||
| Media-Storage | `spatie/laravel-medialibrary` |
|
||||
| Audit | `spatie/laravel-activitylog` |
|
||||
| Settings | `spatie/laravel-settings` |
|
||||
| Backup | `spatie/laravel-backup` (4×/Tag, 14d Retention) |
|
||||
| Translations | DB-Table + Redis-Cache (Custom-Implementation, Admin-UI) + Spatie-Translatable für Model-Felder |
|
||||
| Subscriptions | Laravel Cashier (Stripe) |
|
||||
| Queue | Redis + Horizon |
|
||||
| Search | Eloquent `WHERE LIKE` (MySQL FULLTEXT optional) |
|
||||
| Testing | Pest 3 + Livewire-Testing + Browser-Tests |
|
||||
| Static Analysis | PHPStan Level 6+ |
|
||||
| Code-Style | Laravel Pint |
|
||||
| CI/CD | GitHub Actions → SSH-Deploy zu Hetzner |
|
||||
| Container | Docker Compose |
|
||||
| Filesystem | Lokal NAS-Mount (dev), Hetzner Storage Box (prod) |
|
||||
|
||||
---
|
||||
|
||||
## 4. Modul-Struktur (Domain-Driven)
|
||||
|
||||
```
|
||||
app/
|
||||
├─ Domains/
|
||||
│ ├─ Workspace/ Tenant-Container, Members, Invitations
|
||||
│ ├─ Domain/ Custom Domains, SSL-Provisioning
|
||||
│ ├─ Link/ Short-Links, Variants (A/B)
|
||||
│ ├─ QrCode/ QR-Generator, Templates, Renderers
|
||||
│ ├─ Bio/ Link-in-Bio Microsites
|
||||
│ ├─ Analytics/ Click-Tracking, Rollups, Queries
|
||||
│ ├─ Routing/ Smart-Routing Rule-Engine, A/B, Pixel-Inject
|
||||
│ ├─ Subscription/ Stripe-Billing, Plan-Limits
|
||||
│ ├─ Translation/ i18n Admin
|
||||
│ ├─ Ai/ Phase-1 AI-Suite
|
||||
│ ├─ Webhook/ Outgoing-Webhooks
|
||||
│ ├─ Api/ REST-API-Layer
|
||||
│ └─ Ads/ Phase-3 (Meta + Google + TikTok)
|
||||
├─ Http/
|
||||
│ ├─ Controllers/ RedirectController (Hot-Path, KEIN Livewire)
|
||||
│ ├─ Middleware/ ResolveWorkspace, EnforceLimits, LocaleFromUser
|
||||
│ └─ Resources/ API-Transformers
|
||||
├─ Livewire/
|
||||
│ ├─ Layouts/ AppLayout, GuestLayout, PublicBioLayout
|
||||
│ └─ Pages/ Per Domain
|
||||
└─ Console/
|
||||
└─ Commands/ SyncStripe, ProvisionSsl, ResolveDomains, Rollups
|
||||
```
|
||||
|
||||
**Architektur-Prinzipien:**
|
||||
|
||||
1. **Domain-Folder-Struktur** statt Type-Folders.
|
||||
2. **Actions-Pattern** für alle Writes (`CreateLink`, `UpdateLink`, etc.).
|
||||
3. **Alle Views via Livewire-Components.** Normale Controller nicht für Views nutzen. Ausnahme: `RedirectController` (kein View, nur 302-Response, Hot-Path).
|
||||
|
||||
---
|
||||
|
||||
## 5. Datenmodell
|
||||
|
||||
### Kern-Tabellen
|
||||
|
||||
```sql
|
||||
-- USERS
|
||||
users
|
||||
├─ id, ulid, name, email (unique), password
|
||||
├─ email_verified_at, two_factor_secret, two_factor_recovery
|
||||
├─ locale (de/en), theme (dark/light/system)
|
||||
├─ default_workspace_id (nullable, fk)
|
||||
└─ timestamps, soft_deletes
|
||||
|
||||
-- WORKSPACES (Tenants)
|
||||
workspaces
|
||||
├─ id, ulid (unique, in URL), name, slug (display)
|
||||
├─ owner_id (fk users)
|
||||
├─ plan_id (fk plans), trial_ends_at
|
||||
├─ branding (json: logo, primary_color, accent_color)
|
||||
├─ stripe_id, stripe_customer_metadata (json)
|
||||
└─ timestamps, soft_deletes
|
||||
|
||||
workspace_members
|
||||
├─ id, workspace_id (fk), user_id (fk)
|
||||
├─ role (enum: owner, admin, editor, viewer)
|
||||
├─ invited_by, joined_at
|
||||
└─ unique(workspace_id, user_id)
|
||||
|
||||
workspace_invitations
|
||||
├─ id, workspace_id, email, role, token (unique)
|
||||
├─ invited_by, expires_at, accepted_at
|
||||
└─ timestamps
|
||||
|
||||
billing_profiles
|
||||
├─ id, workspace_id (fk), billing_user_id (fk users)
|
||||
├─ company_name (nullable), vat_number (nullable, VIES-validated)
|
||||
├─ address_line_1, address_line_2, city, postal_code, country
|
||||
├─ email_billing
|
||||
└─ timestamps
|
||||
|
||||
-- DOMAINS
|
||||
domains
|
||||
├─ id, workspace_id (fk), hostname (unique)
|
||||
├─ type (enum: short, bio, both)
|
||||
├─ verified_at, verification_token
|
||||
├─ ssl_status (pending/active/failed), ssl_issued_at, ssl_expires_at
|
||||
├─ is_default (bool, true für system nimu.li)
|
||||
└─ timestamps
|
||||
|
||||
-- LINKS
|
||||
links
|
||||
├─ id, ulid, workspace_id (fk), domain_id (fk, nullable → nimu.li default)
|
||||
├─ slug (varchar 64, indexed mit domain_id unique)
|
||||
├─ target_url (text)
|
||||
├─ title, description, favicon_url
|
||||
├─ status (active/disabled/expired)
|
||||
├─ password (nullable, encrypted)
|
||||
├─ expires_at, click_limit
|
||||
├─ rules (json: smart-routing-config)
|
||||
├─ pixel_config (json: meta_id, google_id, tiktok_id)
|
||||
├─ tags (json array)
|
||||
├─ created_by, updated_by
|
||||
└─ timestamps, soft_deletes
|
||||
|
||||
link_variants -- A/B-Testing
|
||||
├─ id, link_id (fk), label, target_url, weight
|
||||
├─ clicks_count, conversions_count
|
||||
└─ timestamps
|
||||
|
||||
-- QR CODES
|
||||
qr_codes
|
||||
├─ id, ulid, workspace_id, link_id (nullable für direct-QR)
|
||||
├─ type (url/vcard/wifi/pdf/mp3/text)
|
||||
├─ payload (json), style (json: colors, logo, frame)
|
||||
├─ is_dynamic, scan_limit, expires_at
|
||||
└─ timestamps, soft_deletes
|
||||
|
||||
-- LINK-IN-BIO
|
||||
bio_pages
|
||||
├─ id, ulid, workspace_id, domain_id (nullable)
|
||||
├─ slug (unique mit domain_id), title, description, og_image
|
||||
├─ theme (json), is_published
|
||||
└─ timestamps, soft_deletes
|
||||
|
||||
bio_blocks
|
||||
├─ id, bio_page_id (fk), position, type
|
||||
├─ config (json: type-specific)
|
||||
└─ timestamps
|
||||
|
||||
-- ANALYTICS (Raw, 90 Tage retention, monatlich partitioniert)
|
||||
clicks
|
||||
├─ id (bigint), link_id (fk), variant_id (nullable)
|
||||
├─ workspace_id (denormalized)
|
||||
├─ clicked_at (indexed)
|
||||
├─ ip_hash (sha256 mit daily-rotating salt — DSGVO)
|
||||
├─ country, city, device, os, browser
|
||||
├─ referrer_host, referrer_path
|
||||
├─ utm_source, utm_medium, utm_campaign, utm_content, utm_term, language
|
||||
└─ append-only, no updates
|
||||
|
||||
-- ANALYTICS Rollups
|
||||
clicks_hourly (link_id, hour, country, device, count, unique_visitors)
|
||||
clicks_daily (link_id, day, country, count, unique_visitors)
|
||||
clicks_by_referrer (link_id, day, referrer_host, count)
|
||||
clicks_by_utm (link_id, day, utm_source, utm_medium, utm_campaign, count)
|
||||
|
||||
-- BILLING
|
||||
plans
|
||||
├─ id, name, stripe_price_id, monthly_price_cents
|
||||
├─ workspace_limit, member_limit, custom_domain_limit
|
||||
├─ link_limit, click_limit_monthly
|
||||
└─ features (json)
|
||||
|
||||
subscriptions, subscription_items (Cashier-Standard)
|
||||
|
||||
-- TRANSLATIONS
|
||||
translations
|
||||
├─ id, locale (de/en), key, value (text)
|
||||
├─ group, namespace, updated_by
|
||||
└─ unique(locale, namespace, group, key), INDEX(locale, group)
|
||||
|
||||
-- API + WEBHOOKS
|
||||
api_tokens (Sanctum-Standard)
|
||||
webhooks
|
||||
├─ id, workspace_id, url, secret, events (json), is_active
|
||||
└─ timestamps
|
||||
|
||||
webhook_deliveries
|
||||
├─ id, webhook_id, event, payload, status, response_code, attempts
|
||||
└─ timestamps
|
||||
|
||||
-- AUDIT
|
||||
activity_log (Spatie-Standard)
|
||||
```
|
||||
|
||||
### Redis-Keys
|
||||
|
||||
```
|
||||
link:{slug} Hash (default nimu.li-Domain)
|
||||
link:{domain_id}:{slug} Hash (Custom-Domain)
|
||||
→ target, status, rules_json, expires_at, workspace_id, pixel_config
|
||||
|
||||
clicks:{link_id}:{date} Counter (INCR pro Klick)
|
||||
clicks:{workspace_id}:monthly:{ym} Counter für Plan-Enforcement
|
||||
|
||||
lang:{locale} Hash mit allen Translation-Keys
|
||||
workspace:{ulid} Hash mit cached Settings
|
||||
|
||||
ratelimit:redirect:{ip} Sliding-Window-Counter
|
||||
session:{id} Laravel-Session
|
||||
|
||||
ab_sticky:{link_id}:{ip_hash} ULID des Variants, TTL 24h
|
||||
```
|
||||
|
||||
### Wichtige Constraints
|
||||
|
||||
- `(domain_id, slug)` unique — gleicher Slug auf verschiedenen Domains erlaubt
|
||||
- `clicks` append-only
|
||||
- Soft-Deletes auf User-Content
|
||||
- Stripe-Customer auf `workspaces`, nie auf `users`
|
||||
- IP-Adresse niemals roh — immer SHA256(ip + daily_salt) mit Salt-Rotation nach 24h
|
||||
|
||||
---
|
||||
|
||||
## 6. Redirect-Flow + Smart-Routing
|
||||
|
||||
### Request-Pipeline
|
||||
|
||||
```
|
||||
1. GET https://nimu.li/x7k
|
||||
2. nginx → app (Octane)
|
||||
3. RedirectController::__invoke('x7k')
|
||||
4. Domain-Resolve via Cache (Host-Header)
|
||||
5. Redis HGETALL link:{domain_id}:{slug} (Fallback DB)
|
||||
6. Pre-Checks: status, expires_at, click_limit, password, plan_click_limit
|
||||
7. Bot-Detection (UA-Pattern, optional AbuseIPDB)
|
||||
8. Smart-Routing-Resolve (Rules + Context: geo/device/os/time/lang/referrer/utm)
|
||||
9. A/B-Variant-Pick mit Sticky-Hash (IP+Link, 24h Redis-TTL)
|
||||
10. Pixel-Injection-Decision (Interstitial-HTML oder direkt 302)
|
||||
11. Fire-and-forget Click-Event (Redis-INCR + Queue-Job)
|
||||
12. 302-Response (Ziel <10ms p95)
|
||||
```
|
||||
|
||||
### Smart-Routing Rule-Engine
|
||||
|
||||
JSON-Konfiguration pro Link mit Priorisierten Rules:
|
||||
|
||||
```json
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"id": "rule-1",
|
||||
"priority": 1,
|
||||
"conditions": [
|
||||
{ "field": "device", "op": "in", "value": ["mobile"] },
|
||||
{ "field": "os", "op": "equals", "value": "ios" }
|
||||
],
|
||||
"match": "all",
|
||||
"target": "https://apps.apple.com/app/123"
|
||||
},
|
||||
{
|
||||
"id": "rule-2",
|
||||
"priority": 2,
|
||||
"conditions": [
|
||||
{ "field": "country", "op": "in", "value": ["DE","AT","CH"] }
|
||||
],
|
||||
"match": "all",
|
||||
"target": "https://example.de"
|
||||
}
|
||||
],
|
||||
"fallback": "https://example.com"
|
||||
}
|
||||
```
|
||||
|
||||
**Rule-Fields:** `country`, `city`, `language`, `device`, `os`, `time_hour`, `weekday`, `date`, `referrer_host`, `utm_*`
|
||||
|
||||
**Operators:** `equals`, `not_equals`, `in`, `not_in`, `contains`, `starts_with`, `between`, `greater_than`, `less_than`
|
||||
|
||||
**Match-Modes:** `all` (AND), `any` (OR)
|
||||
|
||||
**Engine** evaluiert in Priority-Reihenfolge, erste Match gewinnt. Fallback wenn keine Match.
|
||||
|
||||
### Klick-Verarbeitung
|
||||
|
||||
```
|
||||
RecordClickJob:
|
||||
1. INSERT INTO clicks (...)
|
||||
2. Broadcast(new ClickRecordedEvent($click)) → Reverb → Live-Dashboard
|
||||
3. Wenn Conversion-API enabled (Phase 2): SendMetaCapiJob, SendGoogleConversionJob
|
||||
4. Webhook-Trigger: workspace.webhooks where event='click.recorded'
|
||||
```
|
||||
|
||||
### Rollup-Schedule
|
||||
|
||||
| Cron | Job |
|
||||
|------|-----|
|
||||
| every 15 min | RollupHourlyJob (clicks → clicks_hourly) |
|
||||
| daily 02:00 | RollupDailyJob (clicks_hourly → clicks_daily) |
|
||||
| daily 03:00 | PruneRawClicksJob (DELETE WHERE clicked_at < now()-90d) |
|
||||
| daily 04:00 | CheckSslExpiryJob |
|
||||
| hourly | VerifyPendingDomainsJob |
|
||||
| weekly mon 09:00 | SendWeeklyInsightReportJob (AI-Feature) |
|
||||
| 00:00, 06:00, 12:00, 18:00 | DatabaseBackupJob |
|
||||
|
||||
---
|
||||
|
||||
## 7. Multi-Tenancy
|
||||
|
||||
### Workspace-Modell
|
||||
|
||||
- Single-DB, `workspace_id` Foreign Key auf allen Workspace-Resourcen
|
||||
- ULID statt sequenzieller ID in URL für Unguessability
|
||||
- URL-Pattern: `app.nimuli.com/w/{workspace_ulid}/...`
|
||||
- Solo-User: Auto-Redirect zu `default_workspace_ulid`
|
||||
- Multi-Workspace-User (Agency): URL-Slug bestimmt aktiven Kontext, Multi-Tab-fähig
|
||||
|
||||
### Workspace-Rollen
|
||||
|
||||
| Rolle | Rechte |
|
||||
|-------|--------|
|
||||
| Owner | Alles + Billing + Workspace-Delete |
|
||||
| Admin | Alles außer Billing + Workspace-Delete |
|
||||
| Editor | Links/QR/Bio erstellen+bearbeiten+löschen |
|
||||
| Viewer | Nur Lesen, Analytics |
|
||||
|
||||
### Plan-Limits (vorläufig)
|
||||
|
||||
| Plan | Workspaces/User | Members/Workspace | Custom Domains | Links | Klicks/Monat | Preis |
|
||||
|------|----------------|-------------------|----------------|-------|--------------|-------|
|
||||
| Free | 1 | 1 | 0 (nimu.li only) | 50 | 1.000 | 0€ |
|
||||
| Pro | 1 | 3 | 1 | 1.000 | 25.000 | 19€ |
|
||||
| Business | 3 | 10 | 5 | 10.000 | 250.000 | 79€ |
|
||||
| Agency | unlimited | unlimited | unlimited | unlimited | 5M (pay-as-you-go danach) | 199€ |
|
||||
|
||||
---
|
||||
|
||||
## 8. Billing
|
||||
|
||||
### Stripe-Setup
|
||||
|
||||
- Stripe + Stripe Tax + PayPal als Method + SEPA-Lastschrift
|
||||
- Customer pro Workspace (nicht pro User)
|
||||
- Cashier für Subscription-Management
|
||||
- Webhook-Endpoint mit Signature-Verify + Idempotency-Check
|
||||
|
||||
### Rechnungs-Konzept
|
||||
|
||||
- Rechnung zeigt **Billing-Profile-Daten** (User oder Firma) oben
|
||||
- Custom-Field "Workspace": {workspace.name}
|
||||
- Footer: "Bezahlt für Workspace: {workspace.name}"
|
||||
- VAT-ID-Validation via VIES (EU-VAT-MOSS-Konformität)
|
||||
- Reverse-Charge B2B intra-EU automatisch via Stripe Tax
|
||||
|
||||
### Plan-Enforcement
|
||||
|
||||
- Limit-Checks vor Action (Pre-Flight)
|
||||
- Redis-Counter für Klick-Limit (real-time)
|
||||
- Soft-Block bei 90% Klick-Limit (Warning), Hard-Block bei 100% (Redirect-Page "Plan-Limit erreicht")
|
||||
- Owner-Email bei Limit-Hit + Upgrade-CTA
|
||||
|
||||
---
|
||||
|
||||
## 9. i18n + Translation-System
|
||||
|
||||
### DB-backed Translations
|
||||
|
||||
```
|
||||
DB-Table: translations
|
||||
├─ id, locale, namespace, group, key, value, updated_by
|
||||
└─ unique(locale, namespace, group, key)
|
||||
|
||||
Redis:
|
||||
lang:de → Hash { 'auth.login': 'Anmelden', ... }
|
||||
lang:en → Hash { 'auth.login': 'Sign in', ... }
|
||||
```
|
||||
|
||||
**Helper:**
|
||||
```php
|
||||
__('auth.login')
|
||||
→ Redis HGET lang:{current_locale} auth.login
|
||||
→ Fallback: DB → Cache → Lang-File
|
||||
```
|
||||
|
||||
**Admin-UI:** Translation-Manager (CRUD über Keys, Diff-View zwischen Locales, Bulk-Import/Export, Missing-Key-Auto-Add bei `__()`-Call in Dev-Mode).
|
||||
|
||||
### Sprachen Phase 1
|
||||
|
||||
DE + EN. Phase 2 EU-Expansion: FR, ES, IT, NL.
|
||||
|
||||
### Model-Translations
|
||||
|
||||
`spatie/laravel-translatable` für Bio-Pages (multi-language Bio-Inhalte für internationale Brands).
|
||||
|
||||
---
|
||||
|
||||
## 10. Theme + Design-System
|
||||
|
||||
### Dual-Mode
|
||||
|
||||
- **Dark Mode (Default):** A-Style — CluPilot-inspiriert, Dark-Tech, Neon-Akzente, Mono-Zahlen
|
||||
- **Light Mode:** B-Style — Vibrant-Modern, Gradients (Lila→Pink), friendly
|
||||
|
||||
### Tailwind v4 `@theme`
|
||||
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-bg: #0a0a0f;
|
||||
--color-s1: #111117;
|
||||
--color-s2: #16161e;
|
||||
--color-s3: #1c1c26;
|
||||
--color-b1: rgb(255 255 255 / 6%);
|
||||
--color-b2: rgb(255 255 255 / 10%);
|
||||
--color-t1: #f0f0f5;
|
||||
--color-t2: #8888a0;
|
||||
--color-t3: #44445a;
|
||||
--color-blue: #4f8ef7;
|
||||
--color-green: #3ecf8e;
|
||||
--color-amber: #f5a623;
|
||||
--color-red: #f05252;
|
||||
--font-mono: 'JetBrains Mono', monospace;
|
||||
--font-sans: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
--color-bg: #ffffff;
|
||||
--color-s1: #ffffff;
|
||||
--color-s2: #f9fafb;
|
||||
--color-s3: #f3f4f6;
|
||||
--color-b1: #e5e7eb;
|
||||
--color-b2: #d1d5db;
|
||||
--color-t1: #111827;
|
||||
--color-t2: #6b7280;
|
||||
--color-t3: #9ca3af;
|
||||
--color-blue: #7c3aed;
|
||||
--color-green: #10b981;
|
||||
--color-amber: #f59e0b;
|
||||
--color-red: #ef4444;
|
||||
}
|
||||
```
|
||||
|
||||
**Custom-Style-Regel:** Keine Inline-Styles im Markup außer für Progressbar-Width-Berechnungen.
|
||||
|
||||
### Layout-Struktur
|
||||
|
||||
```
|
||||
┌────────┬─────────────────────────────────────┐
|
||||
│Sidebar │ Topbar (Workspace-Switcher, │
|
||||
│ 240px │ Search, User-Menu, Theme-Toggle) │
|
||||
│fixed │─────────────────────────────────────│
|
||||
│ │ │
|
||||
│ Logo │ Content Area │
|
||||
│ WS-Box │ (Livewire-Page-Component) │
|
||||
│ Nav │ │
|
||||
│ Plan │ │
|
||||
│ Box │ │
|
||||
└────────┴─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Mobile (<768px): Sidebar als Drawer mit Hamburger.
|
||||
|
||||
### Mobile-First
|
||||
|
||||
- Default-Styles für Mobile, dann `md:`/`lg:` für Desktop
|
||||
- Touch-Targets min 44×44px
|
||||
- Drawer-Nav, Bottom-Action-Bars, Swipe-Gesten
|
||||
- PWA-Manifest + Service-Worker (Offline-Read für Analytics)
|
||||
|
||||
---
|
||||
|
||||
## 11. Sicherheit + DSGVO
|
||||
|
||||
### Auth-Härtung
|
||||
|
||||
- Argon2id Passwort-Hashing
|
||||
- 2FA via TOTP + Recovery-Codes
|
||||
- Cookie-Flags: `secure`, `httpOnly`, `SameSite=Lax`
|
||||
- Login-Rate-Limit 5/15min/IP
|
||||
- Pwned-Password-Check via HaveIBeenPwned-API
|
||||
|
||||
### Workspace-Isolation
|
||||
|
||||
- Global Scope `workspace_id = current_workspace`
|
||||
- Policies + Middleware-Check
|
||||
- 404 statt 403 bei Cross-Workspace (Existenz nicht leaken)
|
||||
|
||||
### Input-Validation
|
||||
|
||||
- Mass-Assignment: `$guarded = ['id', 'workspace_id', 'created_by']` global
|
||||
- Validierung via FormRequests
|
||||
- HTMLPurifier auf User-Content (Bio-Blocks)
|
||||
- SVG-Sanitizer auf Logo-Uploads
|
||||
- File-Upload-Whitelist, Mime-Check, EXIF-Strip
|
||||
|
||||
### Webhooks
|
||||
|
||||
- HMAC-SHA256-Signatur `X-Nimuli-Signature` auf Outgoing
|
||||
- SSRF-Schutz: private IP-Ranges blockiert (10/8, 172.16/12, 192.168/16, 127/8, 169.254/16)
|
||||
- Retry mit Exponential-Backoff (5 Versuche)
|
||||
|
||||
### Custom-Domain
|
||||
|
||||
- DNS-TXT-Record-Verifizierung
|
||||
- Let's-Encrypt-ACME-SSL
|
||||
- Renewal-Cron 30d vor Expiry mit Alerts
|
||||
|
||||
### Redirect-Härtung
|
||||
|
||||
- Rate-Limit 100 req/min/IP (Sliding-Window-Redis)
|
||||
- Bot-Detection
|
||||
- Google-Safe-Browsing-Check auf target_url bei Create + Daily-Re-Check
|
||||
- Phishing-Domains werden auto-disabled
|
||||
|
||||
### DSGVO
|
||||
|
||||
- IP-Hash mit täglich-rotierendem Salt (24h irreversibel)
|
||||
- Cookie-Banner-Logik in Bio-Pages (per Workspace-Setting)
|
||||
- AVV-PDF auto-generierbar
|
||||
- Datenexport (Art. 20): JSON-Bundle, Hintergrund-Job, Signed-URL 7d
|
||||
- Recht auf Vergessen: 30d Soft-Delete → Hard-Delete
|
||||
- Drittland-Transfers (Stripe, OpenAI/Anthropic, MaxMind): SCC + DPF aktiviert
|
||||
- EU-Hosting (Hetzner DE)
|
||||
|
||||
### Security-Headers
|
||||
|
||||
- Strict-CSP, HSTS, X-Frame-Options DENY, X-Content-Type-Options nosniff, Referrer-Policy strict-origin
|
||||
|
||||
---
|
||||
|
||||
## 12. Backup + Disaster-Recovery
|
||||
|
||||
- `spatie/laravel-backup`
|
||||
- 4× täglich MySQL-Dump (00:00, 06:00, 12:00, 18:00)
|
||||
- Retention 14 Tage (= 56 Dumps gespeichert)
|
||||
- Storage: Hetzner Storage Box (separate Region)
|
||||
- Verschlüsselt
|
||||
- Wöchentliche Restore-Drill auf Test-Instanz
|
||||
|
||||
---
|
||||
|
||||
## 13. Test-Strategie
|
||||
|
||||
| Layer | Tool | Ziel |
|
||||
|-------|------|------|
|
||||
| Unit | Pest | Actions, Resolvers, Rule-Engine |
|
||||
| Feature | Pest | HTTP-Routes, Auth, Workspace-Scoping |
|
||||
| Livewire | Pest + Livewire-Testing | Jede Page-Component, Forms, Actions |
|
||||
| Integration | Pest | Stripe-Webhook (mocked), Email, Queue-Jobs |
|
||||
| E2E | Pest 3 Browser | Critical-Paths: Register → Workspace → Link → Klick → Analytics |
|
||||
| Performance | k6 | Redirect p95 <10ms, Dashboard <500ms |
|
||||
| Security | OWASP-ZAP + manuell | Vor Launch |
|
||||
|
||||
**TDD-Approach:** Red-Green-Refactor, Test zuerst. 80%+ Coverage als Smell-Indikator.
|
||||
|
||||
**CI-Pipeline (GitHub Actions):**
|
||||
|
||||
```yaml
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
test:
|
||||
- composer install
|
||||
- npm ci + npm run build
|
||||
- php artisan migrate (test DB)
|
||||
- vendor/bin/pest --parallel
|
||||
- php artisan pint --test
|
||||
- php artisan phpstan analyse
|
||||
deploy (on main):
|
||||
- SSH zu Hetzner
|
||||
- Pull, migrate, restart-Container
|
||||
```
|
||||
|
||||
**Health-Checks:**
|
||||
- `/health` endpoint: DB, Redis, Disk, Queue-Length
|
||||
- UptimeRobot/BetterStack 1min-Polling
|
||||
- Status-Page öffentlich (status.nimuli.com)
|
||||
|
||||
---
|
||||
|
||||
## 14. Phasen-Roadmap
|
||||
|
||||
**Hinweis:** Big-Bang-Launch Phase 1 (Wunsch des Users, kein Wave-Split). Total 12-16 Wochen Vollzeit.
|
||||
|
||||
### Phase 1 — Core + AI + API/Webhooks (12-16 Wochen)
|
||||
|
||||
**Inhalt:**
|
||||
- Setup, Auth+2FA, Workspaces, Domains, Links, Analytics, Billing, Whitelabel, Theme-Switch, i18n
|
||||
- Smart-Routing, A/B-Testing, Link-Schutz, Pixel-Injection, QR-Codes, Link-in-Bio
|
||||
- AI-Suite (Auto-Slug, A/B-Generator, A/B-Explainer, Routing-per-Prompt, Anomalie-Detection, Insight-Report)
|
||||
- API v1 (REST + Sanctum + Rate-Limit per Plan) + Outgoing-Webhooks
|
||||
|
||||
**Launch-Kriterien:**
|
||||
- Stripe Live-Mode, 4 Pläne kaufbar
|
||||
- DSGVO-Audit clean (AVV, Datenschutz, Cookie-Logik)
|
||||
- Sentry, Uptime-Monitor, Status-Page
|
||||
- Onboarding-Tour, Email-Sequenzen, Hilfe-Docs
|
||||
|
||||
### Phase 2 — 3rd-Party Integrationen (6-8 Wochen)
|
||||
|
||||
- Zapier-App-Listing
|
||||
- Make.com-Modules
|
||||
- Slack OAuth + Notifications
|
||||
- Discord Webhooks
|
||||
- Shopify-App-Listing
|
||||
- Conversion-API: Meta CAPI + Google Enhanced Conversions Server-Side
|
||||
|
||||
### Phase 2.5 (parallel) — Mobile App iOS+Android
|
||||
|
||||
- React Native + Expo
|
||||
- Features: Login, Workspace-Switch, Quick-Link-Create (Share-Extension), Analytics-Read-Only, Push-Notifications, QR-Scanner
|
||||
- Push-Service: Expo Push oder Firebase FCM
|
||||
|
||||
### Phase 3 — Ads-Management (10-14 Wochen)
|
||||
|
||||
- Meta OAuth + Ad-Read + Ad-Create + CAPI-Full
|
||||
- Google Ads OAuth + Read + Create + Enhanced Conversions
|
||||
- TikTok Ads (optional)
|
||||
- Unified Campaign-View Cross-Platform
|
||||
- Budget-Alerts + Auto-Pause-Rules (CPL-basiert)
|
||||
|
||||
### Post-Phase-3 (Optional/On-Demand)
|
||||
|
||||
- ClickHouse-Migration bei >50M Klicks/Monat
|
||||
- Edge-Redirects via Cloudflare Workers (globale Latenz <50ms)
|
||||
- Marketplace für Bio-Templates (Revenue-Share)
|
||||
- White-Label-Reseller-Tier
|
||||
- Custom-Reports-Builder (Drag-Drop)
|
||||
|
||||
---
|
||||
|
||||
## 15. Risiken + Mitigations
|
||||
|
||||
| Risiko | Likelihood | Impact | Mitigation |
|
||||
|--------|-----------|--------|-----------|
|
||||
| Spam/Phishing-Missbrauch | hoch | hoch | Safe-Browsing-Check, Manual-Review-Queue für Free-Tier, Account-Auto-Suspend |
|
||||
| DDoS auf Redirects | mittel | hoch | Rate-Limit Redis, LB-DDoS-Schutz, später Cloudflare-Edge |
|
||||
| MySQL überlastet bei Klick-Spike | mittel | mittel | INSERT async via Queue, Rollups, Plan-Click-Limits |
|
||||
| Custom-Domain SSL läuft ab | hoch | mittel | Cron-Renewal 30/14/7/1d-Alerts |
|
||||
| OpenAI/Anthropic-API-Ausfall | mittel | niedrig | Graceful Degradation, Core läuft weiter |
|
||||
| Stripe-Webhook-Verlust | niedrig | mittel | Idempotency-Store + Daily-Reconciliation-Job |
|
||||
| Hetzner-Ausfall / DB-Korruption | niedrig | hoch | 4×/Tag Backup, 14d Retention, separate Region, Wochen-Restore-Drill |
|
||||
| Konkurrenz launcht ähnliches AI-Feature | hoch | mittel | Iterations-Tempo, Agency-Nische-Fokus |
|
||||
| Solo-Dev-Burnout (Big-Bang 16 Wochen) | hoch | hoch | Vom User akzeptiert; trotzdem regelmäßige Checkpoints, MVP-Mindset |
|
||||
| GDPR-Klage Pixel-Injection | mittel | hoch | Consent-Banner Pflicht-Feature, AVV, transparente Datenschutzerklärung |
|
||||
|
||||
---
|
||||
|
||||
## 16. Offene Punkte für Implementation-Plan
|
||||
|
||||
Diese Details werden im Implementation-Plan konkretisiert:
|
||||
|
||||
- Exakte Migrationen-Reihenfolge
|
||||
- Konkrete Route-Definitionen (Web + API)
|
||||
- Konkrete Livewire-Component-Liste mit View-Pfaden
|
||||
- AI-Provider-Wahl (Anthropic Claude vs. OpenAI vs. beide via Adapter-Pattern)
|
||||
- Reverb-Channel-Definitionen + Authorization-Logic
|
||||
- Stripe-Product/Price-Setup-Skript
|
||||
- Seed-Daten für Demo-Workspace
|
||||
- E-Mail-Template-Liste
|
||||
- Onboarding-Tour-Steps
|
||||
- Marketing-Site (`nimuli.com`) — Stack: Statisches Astro/Next oder Teil der App?
|
||||
|
||||
---
|
||||
|
||||
## 17. Erfolgs-Kriterien Spec
|
||||
|
||||
- [x] Architektur klar (Dev + Prod)
|
||||
- [x] Stack festgelegt
|
||||
- [x] Modul-Struktur dokumentiert
|
||||
- [x] Datenmodell komplett
|
||||
- [x] Redirect-Pipeline beschrieben
|
||||
- [x] Multi-Tenancy-Modell klar
|
||||
- [x] Billing-Konzept (inkl. Rechnungs-Anzeige)
|
||||
- [x] i18n-System mit DB+Redis
|
||||
- [x] Theme + Tailwind-Setup
|
||||
- [x] Sicherheit + DSGVO
|
||||
- [x] Backup-Strategie
|
||||
- [x] Test-Strategie
|
||||
- [x] Phasen-Roadmap
|
||||
- [x] Risiken + Mitigations
|
||||
|
||||
**Spec-Status:** Bereit für `writing-plans`-Skill zur Erstellung des Implementation-Plans.
|
||||
Loading…
Reference in New Issue