← Back to blog

May 25, 2026 · 3 min read

Building Knowbase, Part 1: Auth, Orgs, and the Multi-Tenant Foundation

KnowbaseNestJSPostgreSQLAuth

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:

  1. Organisation membershipowner, admin, or member
  2. Workspace membershipowner, admin, editor, or viewer

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:

  1. User hits GET /auth/google → gets redirected to Google
  2. Google sends them back → the app creates or finds their account
  3. 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.