May 25, 2026 · 3 min read
Building Knowbase, Part 1: Auth, Orgs, and the Multi-Tenant Foundation
Before you can build features, you need something solid to build on.
For Knowbase, that meant getting three things right from the start: authentication, multi-tenancy, and the database schema. Get these wrong early, and you're rewriting them later when it hurts.
What is Knowbase?
Knowbase is a multi-tenant knowledge base SaaS. Think Notion or Confluence, but simpler.
The mental model is straightforward:
- A company creates an Organisation
- A team inside that company creates a Workspace
- That team stores and searches Documents inside their workspace
The stack:
- API: NestJS + Drizzle ORM + PostgreSQL (hosted on Supabase)
- App: Next.js 16, React 19, Tailwind v4, shadcn/ui
How tenancy works
The trickiest design decision was roles. I went with two separate membership layers:
- Organisation membership —
owner,admin, ormember - Workspace membership —
owner,admin,editor, orviewer
Here's the key part: a workspace member doesn't link directly to a user. It links to an organisation_members row.
workspaceMembers: {
workspaceId: uuid,
organisationMemberId: uuid, // FK to organisation_members
role: workspaceRole,
}Why does this matter? If you remove someone from an org, they lose access to all its workspaces automatically. No extra cleanup needed.
Auth: Google OAuth + JWT cookies
I went with Google OAuth only — no email/password. Less to maintain, and most users already have a Google account.
The login flow is simple:
- User hits
GET /auth/google→ gets redirected to Google - Google sends them back → the app creates or finds their account
- Two cookies are set: an access token (lasts 15 minutes) and a refresh token (lasts 7 days)
Both cookies are httpOnly, so JavaScript can't touch them.
The refresh token is never stored as plain text:
const hash = await bcrypt.hash(refreshToken, 10);
await db
.update(users)
.set({ refreshTokenHash: hash })
.where(eq(users.id, userId));Even if someone got a copy of the database, they couldn't use those tokens. Every time a token is refreshed, the old one is invalidated and a new one is issued.
Org slugs vs workspace slugs
Org slugs are user-picked and must be globally unique. They show up in URLs like /organisation/acme-corp.
Workspace slugs are auto-generated: workspace-name-a1b2c3d4. Users never have to think about them, and there's no risk of collisions.
I briefly considered letting users pick workspace slugs too. I'm glad I didn't — it adds friction without much benefit.
The 3-org limit
Each user can own a maximum of 3 organisations. It's a simple check before any new org is created:
const owned = await db
.select()
.from(organisations)
.innerJoin(orgMembers, eq(orgMembers.organisationId, organisations.id))
.where(and(eq(orgMembers.userId, userId), eq(orgMembers.role, "owner")));
if (owned.length >= 3) {
throw new ForbiddenException("Maximum 3 owned organisations allowed");
}Three orgs is plenty for most people. Knowbase is meant for teams, not personal use. If you need more than three, that's a pricing conversation.
What's next
With auth and tenancy sorted, the foundation was done. No flashy features yet — just a working login, org and workspace creation, and a schema I felt good about.
Part 2 is where things get interesting: documents, search, and making Knowbase actually useful.