Building a policy compliance tracker
I built a small full-stack app called PolicySignoff. You create policies with deadlines and an attached document, and everyone signs off on them. You can see at a glance who has and who hasn’t.
It’s a small surface area on purpose. I wanted something I could take all the way through: auth, file storage, and a real deployment, not a demo that stops at the UI.
What I wanted to learn
- React 19, TypeScript, and Vite. Client-side routing, form state, auth flows, and talking to a REST API. I started the client in JavaScript and converted it to TypeScript two days in.
- Laravel 12 and Sanctum, with cookies instead of tokens. The SPA authenticates with session cookies and CSRF protection across subdomains, so nothing sensitive lives in localStorage.
- Presigned uploads. The browser uploads documents straight to MinIO, an S3-compatible object store, using a short-lived URL from the API. The file never touches the Laravel server.
- Docker Compose for everything. Local development starts with one command after a short first-run setup, and production runs on a Linode VPS behind NGINX.
My process
I started with static HTML and Tailwind mockups of every screen: sign in, register, the dashboard, a policy, and the create form. Each one covered its states, like signed, overdue, and validation errors. Then I wrote the API spec as a Markdown contract: the data model, every route, the response shapes, and the errors. The backend and frontend were both built against those two artifacts, so there was almost no rework.
The first commit and the deploy pipeline are six days apart. The whole API went in on day one, and the React pages followed, built from the mockups one at a time.
Details I liked
- Creating a policy is two steps. The client saves the policy first, then asks the API for an upload URL and PUTs the file directly to MinIO. If the upload fails, you can retry without creating a duplicate policy.
- Presigned URLs have to match the host. The signature covers the hostname, so the API signs URLs against MinIO’s public address, the one the browser actually calls. Upload links last 15 minutes, and download links last an hour.
- Signing twice gets a clear answer. A unique constraint on each policy and user pair enforces it at the data layer, and the API answers a repeat sign-off with a 409 instead of a database exception.
- Deploys are one script. It pulls, rebuilds the containers, waits for the database, and runs migrations.
- Testing is a curl script. It walks every API endpoint, including a full upload and download round trip.
Next up
- Policy versioning, with re-attestation when the content changes
- Notifications
- Reporting and exports
- A proper roles and permissions system
- Multi-tenancy
- Webhooks, so tools like Slack or an HRIS can react to sign-off events
The source is on GitHub, and I’m happy to share the live demo if you ask.





