Skip to content

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
Email 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)
Email 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, ноль кода).