- golang-migrate embarqué via embed (db/migrations/*.sql), MigrateDatabase(url) au boot
- ids TEXT 32-hex avec CHECK ~ '^[0-9a-f]{32}$' ; operations idempotentes UNIQUE(device_id, operation_id)
- harness db/migrations_test.go (up → assertions schéma → down, skip si PG indisponible)
- DB dev fraîche vaultdrop_dev (la DB vaultdrop héberge un prototype V2 abandonné)
6.1 KiB
6.1 KiB
VaultDrop
Project Status
Project initialized — mobile/ (React Native / Expo) and backend/ (Go) have scaffolding in place. The SQLite layer (schema v4, migrations, repositories) is implemented and covered by tests.
Architecture
- Backend: Go, Gin HTTP framework, PostgreSQL, Tesseract OCR (system call)
- Frontend: React Native (Expo SDK 57), expo-router, expo-sqlite, expo-file-system (SAF)
Key Commands
# Backend
cd backend && go run cmd/server/main.go
# PostgreSQL (via docker-compose)
docker compose up postgres -d
# Frontend
cd mobile && npx expo start
# Typecheck frontend
cd mobile && npx tsc --noEmit
# SQLite layer tests (migrations + repositories)
cd mobile && npm run test:db
Backend Structure
- Entry point:
backend/cmd/server/main.go(wiring gin + config + routes) config/— env (godotenv, optionnel) + defaults:PORT,DATABASE_URL,UPLOAD_DIR,MAX_FILE_SIZE_MB,OCR_LANG, secret pasetomodels/— domain entities (users, devices, documents/resources, clients)service/— business logic (permissions, upload, create folder, move)handlers/— HTTP handlers (bind the routes; currently 501 not-implemented stubs)repository/— Postgres persistence (golang-migrate+lib/pq); IDs are TEXT 32-hex (never UUID conversion, cf.docs/api-v1.md)db/— package migrations (golang-migrate/v4, embarquées viaembeddansdb/migrations/*.sql) :db.MigrateDatabase(url)au boot du serveur ; test harnessdb/migrations_test.go(up → assertions schéma → down,TEST_DATABASE_URL, skip si PG indisponible)ocr/— OCR engine behind an interface (Tesseract system call,OCR_LANGdéfautfra+eng)- Response helpers:
pkg/api/response.go - File uploads stored in
backend/uploads/ - Standard JSON response envelope:
{ "data": ..., "meta": { "page": ..., "total": ... } } - Error format:
{ "error": { "code": "...", "message": "..." } } - Route list is a tracked contract (
cmd/server/router_test.gomirrorsmobile/api/client.ts)
Frontend Structure
- Entry point:
mobile/App.tsx(expo-router layout + Auth context) - Data layer —
mobile/services/:safDirectory.ts+safDirectory.types.ts: physical access viaexpo-file-system(pick/list/create, Documents/SAF uris)db/— SQLite persistence, seemobile/AGENTS.mdfor the full contract (schema, migrations, repositories, tests)localStorage.ts— thin re-export ofservices/db(legacy alias)
features/syncDevice.ts— device sync orchestration (two-pass SAF walk, single transaction per root,exists = 0reconciliation)context/AuthContext.tsx— session context: exposesdeviceUserId(bootstrapped fromgetDeviceUserId()) and starts the backgroundsyncDeviceloopapp/— expo-router screens:index.tsx(dossiers racines + ajout SAF),folder/[id].tsx(sous-dossiers + fichiers)api/— REST client (client.tsfetch wrapper +types.ts= contrat d'API : enveloppe{ data, meta }, erreurs{ error: { code, message } })hooks/— TanStack Query hooks:useFiles,useSearch,useUpload(+ OCR jobs)- No business logic on the client — heavy processing stays server-side
- API base URL via
EXPO_PUBLIC_API_BASE_URL(défauthttp://localhost:8080/api/v1)
Data Conventions
- Canonical identity for folders/files/shares/share_links is
resource_id: opaquelower(hex(randomblob(16))), generated locally, never reused. The physicaluriis nullable (NULL = cloud-only) and is the reconciliation key for the SAF walk. owner_idis NOT NULL on every folder/file row, seeded from the device'sdevice_user_id.- Folder/file
sync_statusis a placement state:local|cloud|local-cloud(transitions viatransitionSyncStatus). It is not a push progress marker. - Shares/share_links carry no
sync_status; theirpushStatus(pending/synced/failed) is derived from thepending_operationsoutbox. - Decisions are made offline from a cached
resource_permissionssnapshot pushed by the server; the server remains the source of truth.canAccessenforces ranking (viewer < commenter < editor < owner),inherit,expires_at, and a 24h stale-cache read-only downgrade. password_hashand download counters are server-side only; the client only stores thehas_passwordboolean and a counter mirror.
API Contract (V1)
- The mobile client is the contract: endpoint shapes in
mobile/api/types.ts+mobile/api/client.tsare authoritative and must match exactly; the server does not renegotiate them. Consolidated spec:docs/api-v1.md. - Identity: device-first. The device registers (
POST /devices) and authenticates with a paseto bearer token; no user accounts in V1 (userstable exists butdevices.user_idstays NULL). - Identifiers:
resource_id/device_user_id/ share-linktokenare opaque lowercase 32-hex TEXT (^[0-9a-f]{32}$, CHECK-enforced), stored as-is server-side (no UUID conversion). The mobile always generateslower(hex(randomblob(16))). - Outbox idempotence + ordering: client pushes batches of
pending_operations; each operation carriesoperation_id(= clientpending_operations.id), server enforces uniqueness per device. Batches are applied sequentially; the server stops at the first non-idempotent failure and returns the index reached so the client resumes there (outbox retry/backoff can reorder). - Permissions snapshot: the server pushes
resource_permissionssnapshots (effective_accessranking viewer < commenter < editor < owner,inherit,expires_at, TTL 24h → read-only downgrade) that the offlinecanAccessconsumes.
Non-Goals (V1)
- Plugin system
- On-device OCR
- Full multi-tenant federation / public discovery
- Multi-writer sync conflicts (single-owner device identity; device-local
device_user_id)
References
docs/api-v1.md— contrat API V1 (autoritatif, consolidé depuismobile/api/types.ts)README.md/V1.md— specs obsolètes (bannières en tête de fichier)V2.md— modèle cible Postgres/ReBAC (identifiants en TEXT 32-hex, cf.docs/api-v1.md)