An API-only backend for a simplified Notion / Google Docs, built with Django REST Framework. Users create workspaces, invite collaborators with role-based access, write versioned documents, comment in threads, tag documents, and have every important action audit-logged automatically.
There is no frontend — Postman is the client. All endpoints live under /api/.
- Tech stack
- Features
- Prerequisites
- Setup
- Configuration (environment variables)
- Running the server
- Applying migrations
- Data models
- API reference (17 endpoints)
- Architecture highlights
- Project structure
- Postman collection
- Demo video
- Switching to PostgreSQL for submission
| Layer | Choice |
|---|---|
| Language | Python 3.12+ |
| Web framework | Django 6.x |
| API framework | Django REST Framework 3.x |
| Database | PostgreSQL (production) · SQLite (local dev) |
| Env / packaging | uv + django-environ |
- Workspaces with role-based membership (
admin,editor,viewer); the owner is auto-enrolled as anadminmember in the same transaction. - Versioned documents — every create and every update snapshots a new
DocumentVersion, numbered per document. - Threaded comments via a self-referential foreign key (a comment can reply to another).
- Tagging documents through a many-to-many relationship, with filtering by tag.
- Aggregated stats — workspace detail, workspace summary, and document stats endpoints
built with
annotate()/aggregate()andCount. - Automatic audit logging — a
post_savesignal onDocumentwrites anAuditLogrow on every create/update. - Custom request-logging middleware — prints method, path, status code, and duration (ms) for every request.
- Python 3.12+
- uv installed
- PostgreSQL — only required for the final submission pass. Local development runs on file-based SQLite with zero setup (see Configuration).
# 1. Clone the repository
git clone <your-repo-url> collabDocs
cd collabDocs
# 2. Install dependencies into a project-local virtual environment
uv sync
# 3. Create your local environment file from the template
cp .env.example .env
# 4. Generate a Django SECRET_KEY and paste it into .env
uv run python -c "from django.core.management.utils import get_random_secret_key as g; print(g())"
# 5. Apply migrations (creates db.sqlite3 by default)
uv run python manage.py migrate
# 6. Run the server
uv run python manage.py runserverThe API is now available at http://127.0.0.1:8000/api/.
uv syncreadspyproject.toml+uv.lockand installs into.venv/. Prefixing commands withuv runexecutes them inside that environment — no manualactivateneeded.
Copy .env.example to .env and fill in real values. The real .env is git-ignored;
never commit it.
| Variable | Required | Description |
|---|---|---|
SECRET_KEY |
Yes | Django cryptographic secret. Generate a fresh one (see step 4 above). |
DEBUG |
Yes | True for local dev, False in production. Parsed with env.bool(). |
ALLOWED_HOSTS |
Yes | Comma-separated hostnames. May be empty while DEBUG=True. |
NODB |
Yes | Database switch. True → SQLite (zero-setup dev default); False → PostgreSQL. |
DATABASE_URL |
When NODB=False |
PostgreSQL DSN: postgres://USER:PASSWORD@HOST:PORT/NAME. Ignored when NODB=True. |
How the database switch works: settings.py reads NODB. When True, it builds a
SQLite config pointing at db.sqlite3 — no database server to install or run. When
False, it parses DATABASE_URL and connects to PostgreSQL. Switching backends is a
one-line .env change with no code edits.
uv run python manage.py runserverServes the API at http://127.0.0.1:8000/api/. Watch the console — the custom
RequestLoggingMiddleware prints a line per request:
POST /api/documents/ 201 42.317ms
GET /api/workspaces/<uuid>/summary/ 200 8.914ms
# Generate migrations after changing any model
uv run python manage.py makemigrations
# Apply all migrations to the configured database
uv run python manage.py migrateAll migrations are committed and apply cleanly from scratch on an empty database.
Eight models, all with UUIDField primary keys (uuid.uuid4, editable=False):
| Model | Purpose |
|---|---|
User |
Person. Unique email and phone. |
Workspace |
Container owned by a user; owner auto-added as admin member. |
WorkspaceMember |
User ↔ Workspace link with a role. UniqueConstraint on (workspace, user). |
Document |
Titled, versioned content in a workspace, with a status (draft/published/archived). |
DocumentVersion |
Immutable snapshot of a document's content; version_number auto-increments per doc. |
Comment |
Threaded discussion; self-referential parent FK for replies. |
Tag |
Label with a ManyToMany link to documents (declared on Tag). |
AuditLog |
Append-only action record, written automatically by a signal. |
Enums (WorkspaceMember.role, Document.status) use Django TextChoices. See
docs/project_brief.md for full field-level specs.
All routes are namespaced under /api/ and registered via DRF's DefaultRouter.
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/users/ |
Create a user. |
GET |
/api/users/{id}/ |
Retrieve a user. |
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/workspaces/ |
Create a workspace + auto-add owner as admin (atomic). |
GET |
/api/workspaces/{id}/ |
Retrieve a workspace with member count. |
POST |
/api/workspaces/{id}/members/ |
Add a member with a role (returns 409 on duplicate). |
GET |
/api/workspaces/{id}/members/ |
List members with their roles. |
GET |
/api/workspaces/{id}/summary/ |
Document count, member count, total comments. |
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/documents/ |
Create a document + first version (atomic). |
PUT |
/api/documents/{id}/ |
Update content + create a new version (atomic). |
GET |
/api/documents/ |
List with search & filtering (Q objects, __icontains, tag). |
GET |
/api/documents/{id}/versions/ |
List all versions, newest first. |
GET |
/api/documents/{id}/stats/ |
Version count, comment count, contributor count. |
POST |
/api/documents/{id}/tags/ |
Add one or more tags to the document. |
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/comments/ |
Add a top-level comment or a reply (via parent). |
GET |
/api/comments/?document={id} |
List all comments for a document (threaded). |
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/tags/ |
Create a tag (unique name). |
GET |
/api/audit-logs/ |
List audit logs, filterable by actor ID and date range (query params). |
- Transactions & integrity — Workspace creation (
create()override) and document save/version creation (create()+update()overrides) each run inside a singletransaction.atomic()block, so a failure rolls back the whole operation. DuplicateWorkspaceMemberinserts are caught asIntegrityErrorand returned as HTTP 409; missing objects raiseDoesNotExist→ 404. - Signals —
audit_logs/signals.pydefines apost_savereceiver onDocument, connected inAuditLogsConfig.ready(). It usesinstance._state.addingto distinguishcreatedfromupdatedand writes anAuditLogrow each time. - Middleware —
config/middleware.py::RequestLoggingMiddleware, registered last insettings.MIDDLEWARE, times each request and prints method, path, status, and duration. - Query optimisation —
select_related()on endpoints returning nested user/workspace/ document data;annotate()/aggregate()withCounton the stats/summary/detail endpoints;Qobjects for OR search on the document list.
collabDocs/
├── config/ # Project settings, URLs, WSGI/ASGI, middleware
│ ├── settings.py # INSTALLED_APPS, MIDDLEWARE, DB switch (NODB)
│ ├── urls.py # /api/ router includes for every app
│ └── middleware.py # RequestLoggingMiddleware
├── users/ # User model, serializer, viewset, urls
├── workspaces/ # Workspace + members/summary actions
├── workspace_members/ # WorkspaceMember model + UniqueConstraint
├── documents/ # Document + versions/stats/tags actions
├── document_versions/ # DocumentVersion model
├── comments/ # Threaded comments (self-referential FK)
├── tags/ # Tag model (M2M to Document)
├── audit_logs/ # AuditLog model + post_save signal (ready())
├── docs/ # project_brief.md and tickets
├── manage.py
├── pyproject.toml / uv.lock
├── requirements.txt # Pinned deps (mirror of the uv lockfile)
└── .env.example # Environment template (copy to .env)
Each app follows the standard Django layout (models.py, serializers.py, views.py,
urls.py, migrations/) so responsibilities are easy to find and safe to split across
teammates.
A Postman collection covering all 17 endpoints — organised into Users, Workspaces,
Documents, Comments, Tags, Audit Logs folders, with sample request bodies for every
POST/PUT — is committed at the repository root as CollabDocs.postman_collection.json.
Import it into Postman: Import → File → select the .json. Set the collection variable
base_url to http://127.0.0.1:8000.
📹 Walkthrough (5–10 min): <add your Loom / Google Drive link here>
The video demonstrates:
- A complete atomic transaction with rollback on failure.
- Middleware logs appearing in the console during requests.
- At least one aggregation endpoint (document stats / workspace summary).
- An
AuditLogwritten by the signal after a document update.
Local development uses SQLite for speed, but the rubric expects PostgreSQL. Before submitting:
# 1. Create the database and user in Postgres
# createdb collabdocs (and a matching role/password)
# 2. In .env, flip the switch and set the DSN
# NODB=False
# DATABASE_URL=postgres://collabdocs:password@localhost:5432/collabdocs
# 3. Re-run migrations against Postgres from scratch
uv run python manage.py migrate
# 4. Re-run the full Postman collection to confirm everything passesNo code changes are required — only .env.
docs/project_brief.md— full spec, data models, endpoint list, and evaluation rubric.docs/tickets/— feature-by-feature implementation tickets.