Multi-tenant Team Knowledge Base SaaS
A centralized platform where teams can create, organize, and share internal documentation, processes, guides, and institutional knowledge.
# 1. Clone and configure
cp .env.example .env
# 2. Start with Docker
docker compose up -d
# 3. API is available at
open http://localhost:8000/docs# Create virtual environment
python -m venv .venv
source .venv/bin/activate
# Install dependencies
pip install -e ".[dev]"
# Run the server
uvicorn app.main:app --reload
# Run migrations
alembic upgrade head
# Run tests
pytest tests/ -vKnowBase/
βββ .env.example # Environment variables template
βββ Dockerfile # Multi-stage Docker build
βββ docker-compose.yml # API + PostgreSQL + Redis
βββ pyproject.toml # Dependencies + ruff + pytest config
βββ alembic.ini # Alembic config
β
βββ alembic/ # Database migrations
β βββ env.py # Async migration environment
β βββ script.py.mako # Migration template
β βββ versions/ # Migration files
β
βββ app/
β βββ main.py # FastAPI app factory + lifespan
β βββ dependencies.py # Shared deps (auth, tenant, RBAC)
β βββ middleware.py # Request logging, CORS
β β
β βββ api/
β β βββ router.py # Aggregates all module routers
β β
β βββ core/ # Infrastructure
β β βββ config.py # Pydantic Settings
β β βββ database.py # Async SQLAlchemy engine + sessions
β β βββ redis.py # Redis connection pool
β β βββ security.py # JWT + password hashing
β β βββ logging.py # Structured logging
β β βββ exceptions.py # Custom exceptions + handlers
β β
β βββ models/
β β βββ base.py # BaseDBModel (id, created_at, updated_at)
β β
β βββ modules/ # Domain modules
β βββ auth/ # JWT auth (register, login, refresh)
β βββ users/ # User profiles
β βββ organizations/ # Tenant organizations
β βββ memberships/ # RBAC (user β org roles)
β βββ workspaces/ # Workspaces within orgs
β βββ documents/ # Knowledge base documents
β βββ document_versions/# Version history
β βββ audit_logs/ # Immutable audit trail
β βββ invites/ # Org invitations
β
βββ tests/
βββ conftest.py # Async fixtures
βββ test_health.py # Health check test
βββ test_auth.py # Auth flow tests
Each module follows the same internal structure:
modules/{name}/
βββ __init__.py
βββ models.py # SQLModel ORM models
βββ schemas.py # Pydantic request/response schemas
βββ repository.py # Data access layer (DB queries)
βββ service.py # Business logic layer
βββ router.py # FastAPI router (API endpoints)
βββ dependencies.py # Module-specific dependencies (if needed)
Request β Router β Service β Repository β Database
β
Schemas (Pydantic)
| Layer | Responsibility |
|---|---|
| Router | HTTP endpoints, request parsing, response serialization |
| Service | Business logic, validation rules, orchestration |
| Repository | Database queries, data persistence |
| Models | ORM definitions (SQLModel) |
| Schemas | Input/output validation (Pydantic v2) |
Modules communicate through their service layer, never directly through repositories or models of another module. For example:
OrganizationServiceimportsMembershipmodel to create the owner membership on org creation.InviteServiceaccepts dependencies forUserRepositoryandMembershipRepositoryto handle invite acceptance.dependencies.py(shared) importsUserandMembershipmodels for auth and RBAC checks.
This keeps boundaries clear β each module owns its data, and cross-module interactions happen through injected dependencies at the service layer.
JWT-based authentication with access + refresh token flow:
| Token | Lifetime | Purpose |
|---|---|---|
| Access Token | 30 minutes | API authentication |
| Refresh Token | 7 days | Issue new access tokens |
Endpoints:
POST /api/v1/auth/registerβ Create account, get tokensPOST /api/v1/auth/loginβ Login with email/passwordPOST /api/v1/auth/refreshβ Refresh expired access token
Auth Dependency:
from app.dependencies import get_current_user
@router.get("/protected")
async def protected_endpoint(user: User = Depends(get_current_user)):
# user is authenticated
...Organization-scoped architecture. All tenant data includes an organization_id column.
How it works:
- User authenticates β JWT contains
user_id - Request includes
org_idin the URL path (e.g.,/api/v1/organizations/{org_id}/documents) get_current_org_iddependency verifies the user is a member of that org- All queries are scoped to
organization_id
from app.dependencies import get_current_org_id
@router.get("/organizations/{org_id}/documents")
async def list_docs(org_id: UUID = Depends(get_current_org_id)):
# org_id is validated β user is a member
...Roles are stored in the memberships table (join between users and organizations):
| Role | Permissions |
|---|---|
| owner | Full access, manage members, delete org |
| admin | Manage content, invite members, update org |
| member | Create/edit documents, create workspaces |
| viewer | Read-only access |
Usage:
from app.dependencies import require_role
from app.modules.memberships.models import RoleEnum
@router.delete("/{doc_id}")
async def delete_doc(
_role: None = Depends(require_role(RoleEnum.owner, RoleEnum.admin)),
):
# Only owner and admin can reach here
...-
Create the module directory:
app/modules/your_module/ βββ __init__.py βββ models.py βββ schemas.py βββ repository.py βββ service.py βββ router.py -
Define the model in
models.py(inherit fromBaseDBModel) -
Create schemas in
schemas.py(PydanticBaseModel) -
Build the repository in
repository.py(acceptAsyncSession) -
Write business logic in
service.py(accept repository) -
Create the router in
router.pywith endpoints -
Register the router in
app/api/router.py:from app.modules.your_module.router import router as your_module_router api_router.include_router(your_module_router)
-
Import the model in
alembic/env.pyfor migration autogeneration -
Generate migration:
alembic revision --autogenerate -m "add your_module table" alembic upgrade head
| Technology | Purpose |
|---|---|
| FastAPI | Web framework |
| PostgreSQL | Primary database |
| SQLModel | ORM (SQLAlchemy + Pydantic) |
| Alembic | Database migrations |
| Redis | Caching / background jobs readiness |
| Pydantic v2 | Validation & serialization |
| python-jose | JWT tokens |
| passlib | Password hashing (bcrypt) |
| Docker | Containerization |
| pytest | Testing |
| Ruff | Linting & formatting |
# Start all services
docker compose up -d
# Rebuild after code changes
docker compose up --build
# View logs
docker compose logs -f api
# Stop all services
docker compose downServices:
- api β
localhost:8000(FastAPI with hot-reload) - postgres β
localhost:5432(PostgreSQL 16) - redis β
localhost:6379(Redis 7)
# Run all tests
pytest tests/ -v
# Run specific test file
pytest tests/test_auth.py -v
# Run with coverage (install pytest-cov first)
pytest tests/ --cov=app --cov-report=term-missing| Method | Endpoint | Description | Auth |
|---|---|---|---|
POST |
/api/v1/auth/register |
Register | β |
POST |
/api/v1/auth/login |
Login | β |
POST |
/api/v1/auth/refresh |
Refresh token | β |
GET |
/api/v1/users/me |
Current user profile | β |
PATCH |
/api/v1/users/me |
Update profile | β |
POST |
/api/v1/organizations |
Create org | β |
GET |
/api/v1/organizations |
List my orgs | β |
GET |
/api/v1/organizations/{org_id}/members |
List members | β (member+) |
POST |
/api/v1/organizations/{org_id}/members |
Add member | β (admin+) |
GET |
/api/v1/organizations/{org_id}/workspaces |
List workspaces | β |
POST |
/api/v1/organizations/{org_id}/workspaces |
Create workspace | β (member+) |
GET |
/api/v1/organizations/{org_id}/documents |
List documents | β |
POST |
/api/v1/organizations/{org_id}/documents |
Create document | β (member+) |
GET |
/api/v1/organizations/{org_id}/documents/{id}/versions |
List versions | β |
GET |
/api/v1/organizations/{org_id}/audit-logs |
Audit logs | β (admin+) |
POST |
/api/v1/organizations/{org_id}/invites |
Send invite | β (admin+) |
POST |
/api/v1/invites/accept |
Accept invite | β |
GET |
/health |
Health check | β |