Zustimm — Технический план. Соло-разработчик, Pre-MVP → MVP
Продукт: B2B HR Compliance Acknowledgement Platform. Мобильное подтверждение ознакомления с HR-документами: Face ID/Touch ID + визуальная подпись + электронный след. Рынок: немецкий Mittelstand 50-500 чел.
Контекст: Соло-основатель в Германии, вайбкодинг с Claude Code. Инфраструктура pAss (домашний сервер, n8n, боты, мониторинг). Бюджет: ~€0 на инфру (используем pAss + Hetzner free/cheap tier). Язык общения с AI: русский. Код, комментарии, коммиты: English.
Граница Pre-MVP / MVP: - Pre-MVP (недели 1-4): Rechtsgutachten + customer dev + landing page. Кода минимум. - MVP (недели 5-12): iOS-приложение + веб-дашборд + PDF-движок. Только core flow.
1. Архитектура
System diagram (текстовое)
┌──────────────────────────────────────────────────────────────┐
│ CLIENT LAYER │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Mobile App │ │ Web Dashboard │ │
│ │ React Native │ │ Next.js 15 │ │
│ │ (iOS only MVP) │ │ (Vercel) │ │
│ │ - Face ID/TouchID │ │ - Upload PDF │ │
│ │ - Draw signature │ │ - Signature │ │
│ │ - View documents │ │ placement │ │
│ │ - Sign documents │ │ - Status dashboard │ │
│ │ - Secure Enclave │ │ - Audit trail view │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
└───────────┼─────────────────────┼─────────────────────────────┘
│ HTTPS (TLS 1.3) │ HTTPS
▼ ▼
┌──────────────────────────────────────────────────────────────┐
│ API LAYER (Hetzner VPS) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ FastAPI (Python 3.12) │ │
│ │ - JWT auth (access + refresh tokens) │ │
│ │ - RBAC (Owner/Admin/Sender/Viewer) │ │
│ │ - Rate limiting (slowapi) │ │
│ │ - Request validation (Pydantic v2) │ │
│ │ - OpenAPI 3.1 auto-docs │ │
│ └──────────────────────┬───────────────────────────────┘ │
│ │ │
│ ┌──────────────────────┴───────────────────────────────┐ │
│ │ SERVICES │ │
│ │ ┌──────────┐ ┌───────────┐ ┌──────────────────┐ │ │
│ │ │ PDF │ │ Signature │ │ Notification │ │ │
│ │ │ Engine │ │ Verifier │ │ Service │ │ │
│ │ │ pikepdf │ │ ECDSA │ │ FCM/APNs/Email │ │ │
│ │ │ PAdES │ │ p256 │ │ (Postmark) │ │ │
│ │ └──────────┘ └───────────┘ └──────────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────┴───────────────────────────────┐ │
│ │ DATA LAYER │ │
│ │ ┌────────────┐ ┌───────────┐ ┌────────────────┐ │ │
│ │ │ PostgreSQL │ │ Redis │ │ MinIO (S3) │ │ │
│ │ │ 16 │ │ BullMQ │ │ PDF storage │ │ │
│ │ │ - Entities │ │ - Queue │ │ - Temp only │ │ │
│ │ │ - AuditLog │ │ - Session │ │ - Auto-expire │ │ │
│ │ │ - Soft del │ │ - Cache │ │ 72h │ │ │
│ │ └────────────┘ └───────────┘ └────────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ INFRASTRUCTURE (pAss home server) │
│ ┌──────────┐ ┌───────────┐ ┌──────────────────────────┐ │
│ │ n8n │ │ Uptime │ │ Grafana + Prometheus │ │
│ │ Workflows│ │ Kuma │ │ + Loki (logs) │ │
│ │ - Alerts │ │ - Monitor │ │ - Metrics dashboard │ │
│ │ - Email │ │ - Health │ │ - Audit log viewer │ │
│ └──────────┘ └───────────┘ └──────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Почему этот стек?
| Компонент | Выбор | Обоснование |
|---|---|---|
| Mobile | React Native (Expo) | Одна кодовая база iOS+Android. Expo SDK даёт Face ID (LocalAuthentication), SecureStore (Keychain/Keystore), push-уведомления из коробки. Нативные модули (Swift/Kotlin) — только для криптографии. |
| Web | Next.js 15 + Vercel | Бесплатный хостинг Vercel для сольного проекта. SSR для SEO (landing page), CSR для дашборда. Никаких серверных расходов на фронтенд. |
| API | FastAPI (Python) | Claude Code отлично пишет на Python. Pydantic v2 = автовалидация. pikepdf/pdfly для PAdES. async из коробки. Один язык для прототипа. |
| DB | PostgreSQL 16 | ACID для audit trail. JSONB для гибких метаданных. Row-Level Security как второй слой RBAC. Бесплатный Hetzner Cloud. |
| Queue | Redis + BullMQ | BullMQ = Redis-based очередь с повторными попытками. Для PDF-обработки. Но: на Pre-MVP — синхронно в FastAPI background task. Redis добавить на неделе 8. |
| Storage | MinIO на Hetzner VPS | S3-совместимый. Временное хранение PDF (72 часа). Бесконечное хранение на своей VPS. В будущем — S3 Glacier для GoBD-архива. |
Что PaaS/SaaS, что self-hosted?
| Слой | Хостинг | Почему |
|---|---|---|
| Web Dashboard | Vercel (Free) | Бесплатно, SSL, CDN, deploy одной командой |
| API + PDF Engine | Hetzner VPS CX22 (€4/мес) | Немецкий ЦОД, 2 vCPU, 4 GB RAM. Достаточно для MVP (до 5000 док/мес) |
| PostgreSQL | Hetzner Cloud (managed, €7.5/мес) | Немецкий ЦОД, backups, никакого администрирования |
| Redis | Самостоятельно на VPS | MVP: достаточно 256 MB. Не платим за managed пока не нужно |
| MinIO | Самостоятельно на VPS | Или Hetzner Object Storage (S3, €5/ТБ) |
| Push-уведомления | FCM/APNs (бесплатно) | Бесплатные сервисы Google/Apple |
| Postmark (€10/мес) | Transactional email. GDPR-совместимый (EU servers) | |
| Мониторинг | pAss (свой сервер) | Uptime Kuma + Grafana уже есть. Бесплатно |
| CI/CD | GitHub Actions (Free) | Бесплатно для публичных/приватных репо |
| Auth | Своя (JWT + refresh) | Не платим за Auth0/Firebase Auth. JWT через PyJWT |
Принцип: всё, что можно бесплатно — бесплатно. Hetzner-расходы = ~€12/мес до первого платящего клиента.
Границы: что строим, что покупаем?
| Строим | Покупаем/используем бесплатно |
|---|---|
| Мобильное приложение (React Native) | Face ID/Touch ID — OS-level API |
| Веб-дашборд (Next.js) | FCM/APNs — Google/Apple |
| API + JWT auth (FastAPI) | Postmark — email delivery |
| PDF-движок: pikepdf + PAdES | eIDAS QES — через D-Trust/Swisscom (фаза Growth) |
| RBAC (своя, 4 роли) | TSA timestamp — через DFN/GlobalTrust (фаза Growth) |
| Audit trail (append-only, хеш-цепочка) | Personio/BambooHR API — фаза Growth |
| ECDSA device-side signing |
2. Data Model
Сущности и связи
Company (1) ─────────── (N) User
Company (1) ─────────── (N) Document
Document (1) ─────────── (N) SignaturePlacement
Document (1) ─────────── (N) SignatureRequest
SignatureRequest (1) ─── (1) Signature (опционально)
Document (1) ─────────── (N) AuditLog
User (1) ─────────────── (N) DeviceKey
User (1) ─────────────── (N) AuditLog
Company (1) ─────────── (N) AuditLog
Поля и типы
Company
id: UUID PK
name: VARCHAR(200) NOT NULL
slug: VARCHAR(50) UNIQUE (для zustimm.io/c/slug)
logo_url: TEXT
billing_email: VARCHAR(255)
settings: JSONB DEFAULT '{}' -- email_templates, branding, default_locale
created_at: TIMESTAMPTZ DEFAULT now()
deleted_at: TIMESTAMPTZ -- soft delete (GDPR Art. 17)
User ``` id: UUID PK company_id: UUID FK → Company email: VARCHAR(255) NOT NULL role: ENUM('owner','admin','sender','viewer') DEFAULT 'sender' name: VARCHAR(200) locale: VARCHAR(5) DEFAULT 'de' email_verified_at: TIMESTAMPTZ created_at: TIMESTAMPTZ deleted_at: TIMESTAMPTZ
UNIQUE(company_id, email) INDEX idx_users_company (company_id) ```
DeviceKey (публичный ключ устройства) ``` id: UUID PK user_id: UUID FK → User NOT NULL device_id: VARCHAR(100) NOT NULL -- platform-generated public_key_pem: TEXT NOT NULL -- ECDSA P-256, SPKI format key_algorithm: VARCHAR(20) DEFAULT 'ECDSA_P256' device_name: VARCHAR(200) -- "iPhone 15 Pro" device_integrity: ENUM('ok','compromised','unknown') DEFAULT 'unknown' attestation: TEXT -- iOS Key Attestation / Android Key Attestation created_at: TIMESTAMPTZ revoked_at: TIMESTAMPTZ
UNIQUE(user_id, device_id) ```
Document ``` id: UUID PK company_id: UUID FK → Company NOT NULL title: VARCHAR(500) NOT NULL document_type: VARCHAR(50) -- 'policy','compliance','nda','other' original_pdf_s3_key: TEXT NOT NULL original_pdf_hash: VARCHAR(64) -- SHA-256 page_count: INTEGER file_size_bytes: INTEGER status: ENUM('draft','sent','partially_signed','fully_signed','expired','revoked') deadline_at: TIMESTAMPTZ message_template: TEXT -- текст push/email уведомления created_by: UUID FK → User created_at: TIMESTAMPTZ deleted_at: TIMESTAMPTZ
INDEX idx_documents_company_status (company_id, status) INDEX idx_documents_hash (original_pdf_hash) -- deduplication ```
SignaturePlacement (место подписи на странице) ``` id: UUID PK document_id: UUID FK → Document NOT NULL page: INTEGER NOT NULL x: FLOAT NOT NULL -- % от ширины y: FLOAT NOT NULL width: FLOAT NOT NULL height: FLOAT NOT NULL label: VARCHAR(200) -- "Employee Signature" order_index: INTEGER DEFAULT 0
INDEX idx_placement_document (document_id) ```
SignatureRequest (запрос на подпись конкретному человеку) ``` id: UUID PK document_id: UUID FK → Document NOT NULL user_id: UUID FK → User NOT NULL status: ENUM('pending','signed','declined','expired') nonce: VARCHAR(64) NOT NULL UNIQUE -- случайный nonce для подписания signed_pdf_s3_key: TEXT -- итоговый PDF с подписью signed_pdf_hash: VARCHAR(64) signature_id: UUID FK → Signature -- заполняется после подписания sent_at: TIMESTAMPTZ signed_at: TIMESTAMPTZ declined_at: TIMESTAMPTZ decline_reason: TEXT reminders_sent: INTEGER DEFAULT 0
UNIQUE(document_id, user_id) INDEX idx_sr_user_status (user_id, status) ```
Signature (криптографическая подпись, созданная на устройстве)
id: UUID PK
signature_request_id: UUID FK → SignatureRequest (UNIQUE)
device_key_id: UUID FK → DeviceKey NOT NULL
signed_hash: VARCHAR(128) NOT NULL -- ECDSA signature (DER-encoded, hex)
document_hash: VARCHAR(64) NOT NULL -- SHA-256 of PDF at signing time
nonce: VARCHAR(64) NOT NULL -- скопирован из SignatureRequest
auth_method: ENUM('face_id','touch_id','pin') NOT NULL
device_timestamp: TIMESTAMPTZ NOT NULL
signature_image_removed_at: TIMESTAMPTZ -- когда визуальная подпись удалена с сервера
created_at: TIMESTAMPTZ DEFAULT now()
AuditLog (append-only, никогда не удаляется) ``` id: UUID PK company_id: UUID FK → Company NOT NULL actor_type: ENUM('user','system') NOT NULL actor_id: UUID -- user_id или NULL для system event_type: VARCHAR(50) NOT NULL -- 'document.uploaded','signature.created', etc. resource_type: VARCHAR(50) NOT NULL -- 'document','signature','user','company' resource_id: UUID NOT NULL metadata: JSONB DEFAULT '{}' ip_address: INET user_agent: TEXT prev_hash: VARCHAR(64) -- хеш предыдущей записи (append-only chain) hash: VARCHAR(64) NOT NULL -- хеш текущей записи
INDEX idx_audit_company_time (company_id, created_at DESC) INDEX idx_audit_resource (resource_type, resource_id) ```
Ключевые индексы (производительность)
idx_sr_user_status— «мои ожидающие подписания» (самый частый запрос)idx_documents_company_status— дашборд HR-менеджераidx_audit_company_time— аудит-лог компании по времениidx_audit_resource— «показать audit trail документа X»- UNIQUE(
document_id,user_id) — один запрос на подпись на человека
3. API Design
REST или GraphQL? Почему REST?
Выбор: REST. GraphQL даёт преимущества только когда клиентов много (web + iOS + Android + public API) и у каждого свои потребности в полях. На Pre-MVP/MVP у нас 2 клиента (web + mobile) с предсказуемыми запросами. REST проще, быстрее в разработке, легче кешировать через CDN, меньше поверхностных расходов на соло-разработку.
Формат: JSON. OpenAPI 3.1 через FastAPI auto-generated docs (/docs).
Аутентификация: JWT + RBAC
``` POST /auth/register Body: { email, password, company_name } → email verification → create Company + User(owner)
POST /auth/login Body: { email, password } → { access_token (15 min), refresh_token (7 days) }
POST /auth/refresh Body: { refresh_token } → { access_token, refresh_token (rotated) }
POST /auth/verify-email/{token} → email_verified_at = now()
POST /auth/forgot-password POST /auth/reset-password/{token} ```
RBAC-модель (4 роли):
- owner: всё в рамках компании, включая биллинг, удаление компании
- admin: управление пользователями, документами, просмотр всего
- sender: загрузка документов, отправка на подпись, просмотр своих документов
- viewer: только чтение документов и статусов (для аудиторов/DPO)
Основные эндпоинты (18)
Documents: ``` POST /v1/documents Body: multipart/form-data { pdf_file, title, document_type } → загрузка PDF, валидация (санитизация JS/embedded files), сохранение в MinIO
GET /v1/documents Query: ?status=draft&page=1&per_page=20 → список документов компании
GET /v1/documents/{id} → метаданные + список SignaturePlacement + статусы SignatureRequest
PUT /v1/documents/{id}/placements Body: { placements: [{page, x, y, w, h, label}] } → сохранить места подписей
POST /v1/documents/{id}/send Body: { recipients: [{email, name}] } → создать SignatureRequest для каждого получателя, отправить push/email
DELETE /v1/documents/{id} → soft delete (GDPR) ```
Signing: ``` GET /v1/signing-requests Query: ?status=pending → запросы на подпись для текущего пользователя (из JWT)
GET /v1/signing-requests/{id} → детали запроса + URL документа (presigned S3)
POST /v1/signing-requests/{id}/sign Body: { signature_hex, device_key_id, auth_method, device_timestamp } → верифицировать ECDSA, встроить подпись в PDF, записать AuditLog → это самый критический эндпоинт
POST /v1/signing-requests/{id}/decline Body: { reason }
POST /v1/devices/register Body: { device_id, public_key_pem, device_name, attestation } → зарегистрировать устройство для пользователя ```
Verification: ``` GET /v1/verify/{document_id} → веб-страница верификации: хеш, подписи, audit trail
POST /v1/verify/upload Body: multipart/form-data { pdf_file } → проверить PDF: есть ли PAdES-подпись, соответствует ли хеш ```
Admin: ``` GET /v1/company/users POST /v1/company/users — пригласить пользователя DELETE /v1/company/users/{id} — soft delete
GET /v1/company/audit-log Query: ?resource_type=document&resource_id=X&page=1
GET /v1/company/settings PUT /v1/company/settings ```
Rate limiting, validation
- Rate limiting: slowapi, 60 req/min для
/auth/*, 120 req/min для/documents/*, 30 req/min для/signing-requests/*/sign(анти-spam подписаний). - Validation: Pydantic v2. Все входные данные валидируются на уровне моделей.
- PDF validation: pikepdf при загрузке — проверка на JavaScript, embedded files, размер (>0, <20MB), число страниц (<100). Невалидный PDF → 422.
- Signature validation: размер подписи (DER < 100 байт), хеш (SHA-256 = 32 байта), nonce (совпадает с SignatureRequest), device_key_id (принадлежит пользователю).
4. Мобильное приложение
React Native архитектура (Expo managed workflow)
src/
├── app/ # Expo Router (file-based navigation)
│ ├── (auth)/ # Неаутентифицированная зона
│ │ ├── login.tsx
│ │ └── register.tsx
│ ├── (app)/ # Аутентифицированная зона
│ │ ├── (tabs)/ # Tab navigator
│ │ │ ├── index.tsx # Список ожидающих подписания
│ │ │ ├── history.tsx # История подписанных
│ │ │ └── settings.tsx
│ │ ├── document/[id].tsx # Просмотр PDF + подписание
│ │ └── signature/create.tsx # Создание визуальной подписи
├── components/
│ ├── SignaturePad.tsx # Canvas для рисования подписи
│ ├── PDFViewer.tsx # react-native-pdf обёртка
│ ├── BiometricButton.tsx # Face ID / Touch ID кнопка
│ └── StatusBadge.tsx # pending/signed/declined
├── services/
│ ├── api.ts # Axios + JWT interceptor
│ ├── crypto.ts # expo-crypto + ECDSA
│ ├── secureStore.ts # expo-secure-store обёртка
│ └── notifications.ts # expo-notifications
├── hooks/
│ ├── useAuth.ts
│ ├── useBiometric.ts
│ └── useDocument.ts
└── utils/
├── keyGeneration.ts # Генерация ECDSA P-256 в Secure Enclave
└── constants.ts
Навигация и экраны
``` Stack (auth) ├── Login ├── Register └── CreateSignature (onboarding, однократно)
Tab Navigator (app, после входа) ├── Pending (список SignatureRequest со статусом pending) │ └── tap → DocumentView (PDF + кнопка "Подписать") ├── History (список подписанных/отклонённых) │ └── tap → DocumentDetail (metadata + audit с сокращённым отображением) └── Settings (профиль, язык DE/EN, выход) ```
Face ID / Touch ID интеграция
typescript
// Ключевой поток подписания:
// 1. Пользователь нажимает "Подписать" на DocumentView
// 2. Вызывается expo-local-authentication:
// const result = await LocalAuthentication.authenticateAsync({
// promptMessage: 'Dokument unterschreiben',
// fallbackLabel: 'PIN verwenden',
// });
// 3. При успехе: приватный ключ извлекается из Secure Enclave
// 4. Подписывается document_hash = SHA-256(document_hash + nonce + timestamp)
// 5. Результат отправляется на сервер: POST /signing-requests/{id}/sign
Ключевые Expo-модули:
- expo-local-authentication — Face ID / Touch ID / PIN
- expo-secure-store — хранение приватного ключа (Keychain на iOS, Keystore на Android). requireAuthentication=true — ключ доступен только после биометрии.
- expo-crypto — SHA-256 хеширование
- expo-notifications — push-уведомления
- react-native-pdf — просмотр PDF
Нативный модуль (единственный вылет из Expo managed): ECDSA-подписание. expo-crypto НЕ поддерживает ECDSA P-256. Решение: написать тонкий нативный модуль (expo-module) или использовать expo-modules-core + Swift/Kotlin для вызова CryptoKit.SecureEnclave.P256.Signing.PrivateKey на iOS и Signature.initSign() с AndroidKeyStore на Android. Это ~50 строк Swift + 50 строк Kotlin.
Оффлайн
MVP: оффлайн не поддерживается (нужен интернет для скачивания PDF). В фазе Growth: AsyncStorage-кеш документов + очередь подписаний.
Push-уведомления
Через FCM (Firebase Cloud Messaging) + APNs (Apple Push Notification service). БЕЗ Firebase как базы данных — только push-канал. expo-notifications обрабатывает и FCM, и APNs.
5. Криптография и подписание
Полный поток: от Face ID до PDF с подписью
``` ШАГ 1: Компания загружает PDF (Web Dashboard) ├── PDF → pikepdf санитизация (удаление JS, embedded files) ├── SHA-256 хеш оригинального PDF → сохраняется в Document └── PDF → MinIO (временное хранение)
ШАГ 2: Подписант получает уведомление ├── Push (APNs/FCM) + email ├── Сервер генерирует nonce (crypto.randomUUID) └── Сохраняется в SignatureRequest
ШАГ 3: Подписант открывает приложение ├── Скачивает PDF по presigned S3 URL ├── react-native-pdf рендерит документ ├── Места подписи подсвечены согласно SignaturePlacement └── Пользователь нажимает "Подписать"
ШАГ 4: Биометрия + подписание (НА УСТРОЙСТВЕ) ├── LocalAuthentication.authenticateAsync() → Face ID / Touch ID ├── При успехе: приватный ключ из Secure Enclave │ (iOS: CryptoKit.SecureEnclave.P256.Signing.PrivateKey) │ (Android: KeyStore.getEntry(keyAlias, null).privateKey) ├── document_hash = SHA-256(PDF_bytes + nonce + timestamp) ├── signature = ECDSA_sign(приватный_ключ, document_hash) │ Результат: DER-encoded, ~70-72 байта └── Отправка на сервер: {signature_hex, device_key_id, auth_method, device_timestamp}
ШАГ 5: Серверная обработка ├── ECDSA_verify(публичный_ключ, document_hash, signature) │ Если не совпадает → 400 (подпись недействительна) ├── pikepdf: вставить визуальную подпись в PDF │ - Открыть PDF, разместить изображение подписи по SignaturePlacement │ - Встроить PAdES-метаданные: хеш документа, метод аутентификации, │ timestamp, device_id, публичный ключ подписанта ├── Сохранить подписанный PDF → MinIO ├── Записать AuditLog (append-only, с хеш-цепочкой) └── УДАЛИТЬ визуальную подпись из памяти сервера ```
PAdES: какой уровень для HR-документов?
Для Compliance Acknowledgement (односторонние подтверждения ознакомления) достаточно PAdES Baseline (B-B): - ГОСТ ETSI EN 319 142-1 - Включает: подпись, сертификат, хеш документа, timestamp подписания - НЕ включает: LTV (long-term validation), TSA-отметку времени, OCSP/CRL
Для QES (фаза Growth): PAdES B-LT (long-term) через eIDAS Qualified TSP (D-Trust, Swisscom).
Secure Enclave / KeyStore
``` Генерация ключа (при регистрации / первом запуске):
iOS (Swift, через expo-module): let privateKey = try SecureEnclave.P256.Signing.PrivateKey( accessControl: SecAccessControlCreateWithFlags( kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .privateKeyUsage, .biometryCurrentSet, // ключ требует СВЕЖУЮ биометрию nil ) ) let publicKey = privateKey.publicKey → публичный ключ (x963, 65 байт) → SPKI PEM → отправка на сервер → приватный ключ остаётся в Secure Enclave НАВСЕГДА
Android (Kotlin, через expo-module): val keyGenerator = KeyPairGenerator.getInstance( KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore" ) keyGenerator.initialize( KeyGenParameterSpec.Builder( keyAlias, KeyProperties.PURPOSE_SIGN ) .setDigests(KeyProperties.DIGEST_SHA256) .setUserAuthenticationRequired(true) // требует биометрию/PIN .setInvalidatedByBiometricEnrollment(true) // новая биометрия → ключ НЕдействителен .build() ) ```
eIDAS Qualified Timestamp
На Pre-MVP/MVP: НЕ нужен. Используем device_timestamp + server_timestamp (из PostgreSQL now()). Для AES (Advanced Electronic Signature) eIDAS не требует TSA.
На фазе Growth: RFC 3161 TSA через DFN (Deutsches Forschungsnetz) — бесплатно для академических/исследовательских, или GlobalTrust/QuoVadis (~€0.01 за timestamp).
6. Compliance & Audit Trail
DSGVO: данные в немецком ЦОД
- Hetzner Cloud: Falkenstein/Nürnberg. Все сервисы (VPS, PostgreSQL, Object Storage) — только немецкие дата-центры.
- Vercel: US-компания, но функции (edge functions) НЕ хранят данные — только рендерят React. Все API-запросы идут напрямую с браузера на Hetzner VPS. Никакие пользовательские данные не проходят через Vercel.
- Postmark: EU-серверы. Только email-адреса для отправки. DPA подписан.
- FCM/APNs: только device token (не email). Не подпадает под GDPR-трансфер (анонимный идентификатор).
BEG IV: требования к документам
BEG IV (Nachweisgesetz, с 1.01.2025) разрешает Textform (§ 126b BGB) для Nachweis существенных условий труда. Textform = email с PDF достаточно. Zustimm идёт дальше — добавляет AES (eIDAS Advanced Electronic Signature), что выше Textform.
Ключевые документы под BEG IV: - Arbeitsvertrag (существенные условия) - Änderungsvereinbarungen - Betriebsvereinbarungen - Arbeitsanweisungen - Datenschutzrichtlinien
Audit trail: что логировать, где хранить
Каждое событие в AuditLog (append-only):
| Событие | actor | resource | Данные в metadata |
|---|---|---|---|
document.uploaded |
user (HR) | document | file_hash, file_size, original_filename |
document.placement_set |
user | document | placements count, pages |
document.sent |
user | document | recipients_count, notification_channel |
document.viewed |
signer | document | device_id, ip, user_agent |
signature.created |
signer | signature | device_key_id, auth_method (face_id/touch_id/pin), device_timestamp, nonce, signed_hash |
signature.verified |
system | signature | verification_result, server_timestamp |
document.declined |
signer | SignatureRequest | decline_reason |
document.reminded |
system | document | reminder_number, sent_to |
document.expired |
system | document | deadline |
document.deleted |
user | document | deletion_reason, gdpr_request_id |
user.registered |
system | user | email, device_attestation |
user.deleted |
user | user | gdpr_request_id |
device.registered |
user | DeviceKey | device_id, attestation_result |
device.revoked |
user | DeviceKey | revocation_reason |
Хеш-цепочка (tamper-evident):
sql
prev_hash = (SELECT hash FROM audit_log WHERE company_id = X
ORDER BY id DESC LIMIT 1)
hash = SHA-256(prev_hash + event_type + resource_id + json(metadata) + timestamp)
Это делает невозможным изменение или удаление одной записи без нарушения всей цепочки.
GDPR Art. 17 vs audit trail
Проблема: Art. 17 требует удаления персональных данных. Audit trail должен сохраняться для compliance (BEG IV, GoBD = 6-10 лет).
Решение:
1. При запросе на удаление (DELETE /v1/users/{id}): deleted_at = NOW(). Пользователь деактивирован, не может входить.
2. AuditLog записи: actor_id обнуляется (SET NULL), email хешируется в metadata. Факт подписания сохраняется, но без привязки к идентифицируемой персоне.
3. Визуальная подпись: удаляется с сервера немедленно после встраивания в PDF. Подписанный PDF, полученный компанией — НЕ модифицируется (это бизнес-запись компании, не персональные данные по Art. 2(2)(c) GDPR).
4. DeviceKey: revoked_at = NOW(). Публичный ключ сохраняется для верификации старых подписей (легитимный интерес, Art. 6(1)(f)).
7. Инфраструктура
Хостинг
| Компонент | Провайдер | Локация | Стоимость/мес |
|---|---|---|---|
| API Server | Hetzner CX22 (2vCPU/4GB) | Nürnberg | €4 |
| PostgreSQL | Hetzner Managed DB | Nürnberg | €7.5 |
| Object Storage | Hetzner Object Storage | Falkenstein | €5/TB |
| Web Dashboard | Vercel Pro | Global CDN | €0 (Free) |
| Postmark | EU | €10 | |
| Monitoring | pAss (home server) | Home | €0 |
| Итого | €26.5 |
Масштабирование: CX22 держит до 5000 документов/мес (запас 5x от MVP). При росте → CX32 (€8), read replica PostgreSQL, dedicated PDF-worker.
CI/CD для соло
GitHub (main branch)
│
├── .github/workflows/api.yml
│ ├── on push to main
│ ├── ruff check + ruff format --check
│ ├── pytest (SQLite in-memory)
│ ├── build Docker image
│ ├── push to GitHub Container Registry (ghcr.io)
│ └── SSH → Hetzner → docker compose pull && up -d
│
├── .github/workflows/web.yml
│ ├── on push to main (web/)
│ ├── next build && next lint
│ └── deploy to Vercel (vercel --prod --token=$VERCEL_TOKEN)
│
└── .github/workflows/mobile.yml
├── on push to main (mobile/)
├── expo doctor
├── eas build --platform ios --profile preview
└── eas submit --platform ios (только для release)
Мониторинг (используя pAss)
- Uptime Kuma: healthcheck
GET /healthкаждые 60 сек. Алерт в Telegram @oniwasoto при падении. - Grafana + Prometheus: метрики FastAPI через
prometheus-fastapi-instrumentator. Дашборд: request rate, latency p50/p95/p99, error rate (4xx/5xx), активные JWT-сессии. - Loki: логи FastAPI в JSON-формате. Поиск по
event_type=signature.created. - Sentry: crash-репортинг для React Native и FastAPI. Бесплатный план (5000 errors/мес).
Бекапы
- PostgreSQL: Hetzner managed — автоматические daily backups, 7 дней хранения. Дополнительно: еженедельный
pg_dump→ Hetzner Object Storage (через cron на VPS). - MinIO / Object Storage: PDF хранятся временно (72 часа). Не бэкапим. Подписанные PDF отдаются компании — компания сама хранит.
- Infrastructure as Code:
docker-compose.yml+.env.exampleв репозитории. Восстановление VPS из CI/CD за 10 минут.
8. План разработки (соло, 12 недель)
Принцип: AI-first, соло. Где AI, где руки.
| Что делает AI (Claude Code) | Что делает человек |
|---|---|
| 100% backend-кода (FastAPI, модели, эндпоинты) | Rechtsgutachten (юридическая экспертиза) |
| 90% фронтенда (React компоненты, стили) | 20 depth interviews с HR-директорами |
| 85% PDF-движка (pikepdf, PAdES-шаблоны) | Customer development: ICP-валидация |
| 80% мобильного приложения (React Native, Expo) | Тестирование на живых людях, а не юнит-тестах |
| DevOps: Docker, docker-compose, GitHub Actions | Разговор с DPO/Betriebsrat компаний |
| Тесты: pytest для API, юнит-тесты для криптографии | Презентация Figma-прототипа дизайн-партнёрам |
Неделя за неделей
Неделя 1: Фундамент + структура проекта
- Инициализация монорепо: api/, web/, mobile/
- FastAPI scaffold: структура проекта, Pydantic модели, Alembic миграции
- PostgreSQL: Company + User таблицы
- JWT auth flow: register, login, refresh
- Docker Compose: api + postgres + redis + minio
- CI/CD: GitHub Actions — lint + test на push
Неделя 2: Document Upload + PDF Engine - PDF upload endpoint: санитизация pikepdf, валидация - PDF metadata extraction: хеш, количество страниц, размер - MinIO integration: загрузка/скачивание PDF - Web dashboard MVP: загрузка PDF + drag-and-drop места подписи (react-pdf) - SignaturePlacement CRUD
Неделя 3: Мобильное приложение — скелет - Expo init, навигация (Expo Router), экраны-заглушки - Auth flow в приложении: login/register экраны - SecureStore: хранение JWT токенов - expo-local-authentication: Face ID / Touch ID проверка - SignaturePad: Canvas для рисования подписи (react-native-gesture-handler + react-native-skia) - Генерация ключевой пары: ECDSA P-256 в Secure Enclave (нативный модуль для iOS)
Неделя 4: Криптография + подписание — ядро - Нативный expo-модуль: генерация ключа в Secure Enclave, подписание хеша - Регистрация DeviceKey на сервере - POST /signing-requests/{id}/sign: полный flow - Серверная верификация ECDSA (cryptography library) - Встраивание визуальной подписи в PDF (pikepdf: overlay image по координатам) - PAdES-метаданные: базовый шаблон (ETSI EN 319 142-1 B-B)
Неделя 5: Notification Pipeline - Push-уведомления: FCM + APNs через expo-notifications - Email: Postmark integration (шаблоны DE/EN) - POST /documents/{id}/send: создание SignatureRequest + отправка уведомлений - Web dashboard: статусы подписания в реальном времени (polling каждые 30 сек)
Неделя 6: Web Dashboard — завершение - Next.js: страницы dashboard, документы, настройки - Детальная страница документа: статус каждого подписанта - Audit log view: фильтрация по event_type, экспорт CSV - Скачивание подписанного PDF + audit trail (ZIP)
Неделя 7: Audit Trail + Compliance
- AuditLog модель + append-only запись на каждое событие
- Хеш-цепочка: SHA-256 связывание записей
- GDPR hard delete: POST /v1/gdpr/delete-account — обнуление персональных данных
- Soft delete для всех сущностей
- Веб-верификация: GET /v1/verify/{document_id} — публичная страница проверки подписи
Неделя 8: Тестирование + полировка API - pytest: 80%+ coverage для критических эндпоинтов - Тест криптографического flow от начала до конца - Валидация: Pydantic edge cases, PDF с вредоносным содержимым - Rate limiting: slowapi конфигурация - OpenAPI-документация: примеры запросов/ответов, описание ошибок
Неделя 9: Мобильное приложение — полировка - История документов (подписанные/отклонённые) - Decline flow: причина отказа - Push-уведомления: deep linking на документ - Локализация: DE/EN строки - App Icon + Splash Screen
Неделя 10: Интеграционное тестирование - End-to-end: загрузка PDF → отправка → push → Face ID → подпись → верификация - Тест на 3 реальных устройствах (iPhone X, 13, 15) - Performance: генерация PDF < 5 сек, API response < 500ms - Uptime Kuma мониторинг healthcheck - Sentry error tracking активация
Неделя 11: Production Readiness - Hetzner VPS: provisioning, Docker Compose деплой - SSL: Let's Encrypt через nginx-proxy + acme-companion - Переменные окружения (Hetzner VPS secrets, не в git!) - Database migrations: Alembic apply на production - Vercel деплой веб-дашборда - Лендинг: zustimm.io с формой waitlist (Next.js pages или separate landing)
Неделя 12: Beta Launch Preparation - TestFlight сборка iOS - Импрессум, Datenschutzerklärung, AGB (шаблоны от Anwalt) - Onboarding flow для design partners (3 компании) - Dashboard walkthrough видео (Loom) - Beta feedback form - Gate: 3 design partners actively using, NPS > 30 → GO to paid
Что НЕ входит в MVP (отложено на Growth)
- Android-приложение (неделя 13+)
- QES через eIDAS TSP (D-Trust/Swisscom)
- TSA timestamp (RFC 3161)
- SSO (OIDC/SAML)
- Интеграции: Personio, BambooHR, SAP
- Оффлайн-режим
- Шаблоны документов
- White-label брендирование
- Batch-экспорт (ZIP всех PDF + audit trail)
- CSV-импорт получателей
- App Clip (iOS) / Instant App (Android)
- Multi-language кроме DE/EN
Ключевые открытые вопросы (требуют ответа ДО написания кода)
| # | Вопрос | Блокирует | Кто решает |
|---|---|---|---|
| OQ-1 | Достаточна ли схема (email + biometric + ECDSA device-side) для eIDAS AES в немецком суде? | Всю архитектуру | Fachanwalt für IT-Recht |
| OQ-2 | Готовы ли пользователи рисовать подпись пальцем? (vs загрузка фото подписи) | Mobile UX | 20 depth interviews |
| OQ-3 | Восстановление при потере устройства: пересоздание ключа + потеря старых подписей — приемлемо? | Key management | Customer dev |
| OQ-4 | Ценообразование: per-envelope или per-company-per-month? | Биллинг (неделя 12) | A/B test с design partners |
| OQ-5 | Нужен ли App Clip / Instant App для подписания без установки приложения? | Mobile adoption | Customer dev |
План утверждён для Pre-MVP (недели 1-4) и MVP (недели 5-12). Соло-основатель + Claude Code. Бюджет инфраструктуры: ~€26.5/мес до первого платящего клиента.
Дата: 2026-06-29. Следующий шаг: Rechtsgutachten + customer development (недели 1-4, ноль кода).