CASE STUDY

AyosDocs

Multi-tenant document management SaaS

A place for Philippine teams to store, organize, find and share their documents and scans, with roles on each project, version history and billing in pesos. I designed and built all of it on my own, from the database schema and API to the interface, tests and deployment pipeline.

Role
Solo developer, design to deployment
Type
Personal project
Status
Pre-launch hardening, October 2026
The AyosDocs workspace

23

API modules

24

Database tables

6 / 29

Roles / permission codes

32

Test files

01.The problem

Many small Philippine teams still keep contracts, permits, invoices and scans in shared drives, chat threads and email attachments. Files get duplicated, the latest version is anyone's guess, and sending one file to a client often means sharing the whole folder.

General-purpose cloud drives solve part of this, but they bill in dollars, don't take GCash or Maya, and aren't built around projects with their own roles. AyosDocs is a document system where each team gets its own isolated workspace, people see only the projects they belong to, and billing works the way local businesses already pay.

02.What it does

Workspaces and roles

Every user gets a personal workspace, and teams create more. Workspaces have Owner, Admin and Member roles, each project has its own Admin, Editor and Viewer, and Business plans can lock folders.

Documents and uploads

Upload files or whole folders by dragging them in. Each document keeps its version history, a preview, signed downloads and an activity timeline, and gets a category and any number of tags.

Search

Postgres full-text search over name, description, tags, category and file name, ignoring accents, with filters for project, folder, type, uploader and date. A quick search (Ctrl K) shows the top five results as you type.

Share links

Send one document to someone outside the workspace. Links can expire, need a password and allow or block downloads, and each one tracks its views. Only a hash of the link is stored.

Billing

Free, Pro and Business plans paid in pesos through PayMongo Checkout (cards, GCash, Maya, QR Ph). Upgrades start right away and credit the unused days. Downgrades wait until the period ends.

Admin and audit

Business workspaces get an audit log with CSV export. A platform super-admin panel manages workspaces, users and payments, can re-run failed webhooks and can grant plans.

FREE

₱0

  • 500 MB storage
  • 2 projects
  • 2 seats
  • 10 MB files

PRO

₱499 / mo

  • 20 GB storage
  • 20 projects
  • 10 seats
  • Share links

BUSINESS

₱2,499 / mo

  • 200 GB storage
  • Unlimited projects
  • 50 seats
  • Audit log, folder locks

03.How tenancy works

The workspace is the tenant. It owns the plan, the storage quota, the seats and the billing. Everything under it belongs to exactly one workspace, and access is decided at two levels.

STRUCTURE

  1. Workspace
  2. Project
  3. Folder
  4. Document
  5. Version

WORKSPACE ROLES

Owner · Admin · Member

Owners and Admins act as project Admin everywhere.

PROJECT ROLES

Admin · Editor · Viewer

Members see only the projects they've been added to.

WHEN A PERIOD ENDS UNPAID

  1. Paid period
  2. 7-day grace
  3. Free plan
  4. Read-only if over limits

Every query filters by workspace. Outsiders get a 404.

Roles map to 29 permission codes, so custom roles can be added later

04.Engineering decisions

404, never 403, across tenants

Every workspace request carries a workspace id, and every query filters on it. A user who isn't a member gets a 404, so nobody can probe which workspace ids exist.

Files never pass through the API

The API checks the role, file type, size and storage left, then signs a one-time upload with a file id it picks itself. The browser sends the file straight to private Cloudinary storage, and a confirm step checks it before the document exists. A background job cleans up uploads that are never confirmed.

Quotas that hold under concurrency

Each write that adds usage locks the workspace row inside its own transaction before it checks the plan's limits. Uploads that finish at the same moment can't add up past the storage quota, and a scenario test checks this.

The plan changes only from the webhook

Only PayMongo's signed webhook changes a plan, never the checkout redirect. Each event is stored by its id, so a repeat does nothing. Events that fail are kept and can be re-run from the admin panel.

Nothing is deleted for unpaid bills

An unpaid period gets a 7-day grace period before the workspace drops to Free. If it's then over Free's limits it goes read-only: viewing, downloading and deleting still work, but uploads and invites pause until the owner upgrades or frees up space.

An audit log that can't be rewritten

Each change is recorded in the same transaction as the change itself, with only the fields that changed. A database trigger rejects any UPDATE or DELETE on the audit table, even when it comes from the API's own connection.

05.Testing

In a multi-tenant app the worst bug is one team seeing another team's files. So the API's Vitest and Supertest suite doesn't just test features. It reads the code's own route and service catalogue and checks the rules on every route.

  • ▸Tests run in-process against a real PostgreSQL database and a real Redis. Only Cloudinary, PayMongo, Resend and Google are mocked.
  • ▸A route catalogue is read from the source, and every route gets 401, 400 and 403 tests for each role, with permission checked before validation.
  • ▸A tenant-isolation test sends another workspace's id to every route that takes an id and expects a 404.
  • ▸Static checks fail the suite if a service query on a workspace table skips the workspace filter, or if a write isn't audited.
  • ▸Scenario tests run a full customer journey and concurrent uploads against the storage quota. A separate load check seeds 100,000 documents and keeps search and listings under 300 ms.

06.Tech stack

  • TypeScript
  • Express 5
  • React 19 (Vite)
  • TanStack Router & Query
  • Base UI
  • Tailwind CSS
  • PostgreSQL
  • Drizzle ORM
  • Redis
  • Socket.IO
  • Cloudinary
  • PayMongo
  • Resend
  • Zod
  • Vitest
  • Docker
  • Caddy
  • Nginx

The public pages (landing, pricing, terms) are prerendered so they load before any script runs. Every green build on main deploys through GitHub Actions to Docker containers behind Caddy for HTTPS, with a health check, Sentry error reports and backups that are tested by restoring them.

Building a SaaS?

I build multi-tenant apps end to end: tenancy, roles, billing and the tests that keep them correct. Let's talk about yours.