Verin
A document management system designed for compliance-first organizations
- Role
- Solo Engineer
- Timeline
- Dec 2025 - Feb 2026
- Stack
- GoChiReactPostgreSQLRedisAsynqS3OpenAPI
Why I Built This
Most document management systems are either enterprise monsters (SharePoint, Box) or toy apps that store files in a folder. I wanted something in between: simple enough for a small team, but with real audit trails, role-based access, and async processing.
The trigger was a compliance project where I needed to track every document version, who accessed it, and when. No existing tool gave me that without a six-figure contract.
How It Works
The API is Go with Chi router - chosen over Gin for its stdlib-compatible middleware chain. Documents upload directly to S3 via pre-signed URLs (the server never touches the file bytes). Once uploaded, a Redis-backed Asynq job fires for OCR extraction and thumbnail generation.
Role-based access is granular: admin, editor, auditor. Auditors can view documents and access logs but can't modify anything. Every action (view, download, edit, delete) is logged to an immutable audit table with timestamp, user, IP, and action type.
The React frontend is generated from the OpenAPI spec - types, API client, and request hooks are all auto-generated. Zero drift between backend and frontend contracts.
Key Decisions
Pre-signed uploads over server proxy - The server never handles file bytes. Clients upload directly to S3 using a pre-signed URL. This eliminated the server as a bandwidth bottleneck and reduced p95 upload latency by 60%. The tradeoff: more complex client-side error handling for multipart uploads.
Asynq over direct processing - OCR and thumbnail generation take 2-8 seconds per document. Processing inline would block the upload response. Asynq (Redis-backed, Go-native) queues these as background jobs with automatic retries. Failed jobs retry 3x with exponential backoff before alerting.
OpenAPI-first development - I wrote the OpenAPI spec before writing any code. The Go server validates requests against the spec at runtime. The React client is generated from it. This front-loaded design time but eliminated an entire class of integration bugs.
What I Learned
Async job processing changes everything about how you think about user experience. The upload feels instant even though processing happens over 8 seconds. But you need to design for partial states - what does the UI show while OCR is running? What happens if the job fails after the user has already navigated away?
Go's simplicity is its superpower for backend services. No ORM, no magic. sql.DB + hand-written queries + proper error handling. It's verbose but I can read any file and understand exactly what it does. After working in TypeScript ORMs, this felt like fresh air.