This README is intended for the developers working on the CourseSpace backend. It contains a concise overview of the project, repo layout, local development steps, testing guidance, and operational notes that help contributors get productive quickly.
- CourseSpaceBackend — internal developer documentation
- Project overview
- Repo layout (important directories)
- Quick start (developer)
- Local development (detailed)
- Running and debugging
- Database & migrations
- Tests and fixtures
- Authentication & authorization notes
- Policy / permission system
- Document storage (S3) and testing considerations
- CI / release notes
- Troubleshooting & tips
- Adding a new feature (checklist)
- Key architectural patterns
- Web framework: FastAPI
- ORM: SQLAlchemy (Declarative models in
src/models.pyand subpackages) - Database: PostgreSQL (migrations via Alembic)
- Background tasks: APScheduler (via
SchedulerManagersingleton) - Storage: S3-compatible object storage for documents (via
S3Managersingleton) - Auth: Firebase Authentication with Bearer tokens (OAuth2PasswordBearer)
- Real-time: WebSocket support for poll updates (via
PollWSManagersingleton) - AI: RAG pipeline with document chunking/embedding and LLM integration (via
AIModelManagersingleton)
The API serves course, thread, comment, document, folder, announcement, calendar, assignment, poll, prompt, notification, report, award, point, and user functionality used by the frontend apps.
src/– application packagemain.py– FastAPI app creation, router registration, lifespan (migrations, scheduler)models.py,schemas.py,services.py(and subpackages) – domain codeauth/,course/,thread/,comment/,document/,folder/,announcement/,assignment/,calendar/,prompt/,user/,policy/,poll/,notification/,report/,award/,point/,scraper/,scheduler/,admin/,dev/– feature modulesdependencies.py– unifiedContextdependency (combinesdb+user)configs.py– environment-driven configuration
tests/– unit/integration tests, fixtures and buildersscripts/– helper scripts (start, setup)alembic/– database migration configs and versionsDockerfile,compose.yaml– container and local compose definitions
Prereqs: Docker + Docker Compose, Python 3.11+, git, VS Code
-
Copy environment variables:
cp .env.example .env # edit .env to set secrets, DB urls, S3 config, etc. -
Reopen this repository in the VS Code devcontainer when prompted.
-
Run the application using
Ctrl + Shift + BorCmd + Shift + B. Alternatively, run:./scripts/start.sh
-
Open the API docs locally:
http://localhost:8000/docs -
Adminer / DB UI:
http://localhost:8080
- Use the devcontainer in VS Code for consistent development environment. Reopen repository in container when prompted.
- Dependencies are installed automatically in the devcontainer with
scripts/setup.sh. - Run the application with
Ctrl + Shift + BorCmd + Shift + B. - The webserver runs on port 8000 by default. Modify
compose.yamlor.envto change ports. - When making schema or model changes remember to:
- Create an Alembic revision:
alembic revision --autogenerate -m "describe change" - Apply the migration in dev manually:
alembic upgrade head - Or run the application which automatically applies migrations on startup.
- Create an Alembic revision:
- Use the FastAPI interactive docs at
/docs(Swagger) for quick API exploration. - Use breakpoints and the Python debugger in VS Code. The devcontainer provides typical Python tooling.
- For local logs and troubleshooting,
logsare written based onlogging.conf; run with-sin tests to see output.
-
Migrations are in
src/alembic/versions/and follow the naming schemeYYYY_MM_DD_HHMM-<hash>_description.py. -
Migrations auto-apply on app startup via
initialise_database()in thelifespancontext. -
To generate a migration:
alembic revision --autogenerate -m "describe change" -
To apply migrations manually (optional, as they auto-apply):
alembic upgrade head
CI/CD applies migrations during deployment — make sure migrations are atomic and tested.
-
Tests live under
tests/and are organized by feature (auth, thread, comment, course, etc.). -
Builders (
tests/**/builders.py) create test data objects WITHOUT DB persistence:thread_data = ThreadBuilder(status=ThreadStatus.ACTIVE).build() # Returns ThreadData
- Builders use method chaining for optional fields (e.g.,
.as_public(),.disallow_comments())
- Builders use method chaining for optional fields (e.g.,
-
Factory fixtures (
tests/**/fixtures.py) create actual DB records:thread = create_thread_factory(user, course) # Committed to test DB
-
All
tests/**/fixtures.pyare auto-loaded as pytest plugins (seetests/conftest.py). -
Tests run in rolled-back transactions for isolation.
-
To run tests:
pytest -q # All tests, quiet output pytest -k test_name # Specific test pytest -s # Show print output
-
Test assertions (from
tests/assertions.py):assert_response(response, 200)- check status, return JSONassert_error_response(response, 404, "THR", "does not exist")- validate error formatassert_response_data(response_data, expected_data)- compare response fields (use"<EXISTS>"for fields that just need to exist)
-
Auth mocking: Use
mock_firebase_auth(client, user)fixture to mock Firebase token verification. This patchesverify_id_tokenand sets Authorization header.
-
Authentication: Firebase Authentication with Bearer tokens (OAuth2PasswordBearer scheme).
-
Routes use unified
Contextdependency combining DB session and authenticated user:from src.dependencies import Context, get_context @router.get("/{id}") @require_permission("resource", "read", "id") def get_resource(id: UUID, context: Context = Depends(get_context)): # Access: context.db (Session), context.user (User)
-
WebSocket routes: Use
get_ws_contextinstead (reads auth from WebSocket headers):from src.dependencies import get_ws_context @router.websocket("/{id}/ws") @require_permission("poll", "read", "id") async def poll_websocket(websocket: WebSocket, id: UUID, context: Context = Depends(get_ws_context)): await wsManager.connect(str(id), websocket)
-
Never use separate
get_dbandget_current_userdependencies — always useContext. -
Key files:
src/auth/dependencies.py— Firebase token validation,get_current_user,get_current_ws_usersrc/dependencies.py—Context,get_context, andget_ws_context(WebSocket)
-
User records auto-created via
get_or_create_useron first authentication. -
Token verification checks: revocation status, user disabled status, token validity.
-
All protected routes use
@require_permission(resource_type, action, resource_id_param)fromsrc/policy/decorators.py. -
Authorization lives in policies—never in routes or services.
-
Decorator extracts
contextfrom route kwargs and validates before execution. -
Policies receive
PolicyContext(db, **route_kwargs)which provides access to DB session and all route parameters via.get(key)method. -
Policies can delegate (e.g.,
ThreadPolicychecksCoursePolicyfor course access). -
To add a new resource policy:
- Create
src/<feature>/policies.pyimplementingBasePolicy.can_perform_action(user, action, resource, context)wherecontextis aPolicyContextinstance - Register at end of
src/policy/managers.py:PolicyManager.register("resource", ResourcePolicy)
- Decorate route:
@require_permission("thread", "update", "id") def update_thread(id: UUID, context: Context = Depends(get_context)): ...
- Create
- Production uses S3-compatible storage. Tests should not upload to production S3.
- For tests prefer mocking S3 calls (see
tests/document/fixtures.py) or usemoto/ a local S3-compatible server. - The document fixtures upload and then clean up S3 objects — ensure tests track created object names for cleanup.
- CI runs unit tests, linting and static analysis. Keep changes that affect public APIs backward compatible or version them.
- When bumping dependencies, update
requirements/and run full test suite locally to ensure compatibility.
- DB connection errors: ensure Docker compose is up and that
.envhas correct DB URL. - Alembic errors: check
env.pyandalembic.inifor correct SQLALCHEMY URL mapping. - S3 issues: verify credentials and endpoint; run S3-integrated tests with a mock.
- HTTP errors: check error code and message for clues, refer to
output.logfor server-side details.
- Create
src/<feature>/models.py- SQLAlchemy models (inherit fromBaseinsrc/models.py) - Create
src/<feature>/schemas.py- Pydantic request/response models - Create
src/<feature>/services.py- business logic withdbas first param - Create
src/<feature>/policies.py- implementBasePolicy.can_perform_action - Create
src/<feature>/routers.py- FastAPI routes with@require_permission - Register policy:
PolicyManager.register("feature", FeaturePolicy)at end ofsrc/policy/managers.py - Register router:
app.include_router(feature_router)insrc/main.py - Create migration:
alembic revision --autogenerate -m "add feature" - Create
tests/<feature>/builders.py,fixtures.py,test_<feature>.py
-
Exception handling: Use typed exceptions with 3-letter module codes:
raise NotFoundException("THR", f"Thread [{id}] does not exist.") raise BadRequestException("CMT", f"Comment [{id}] is deleted.")
Format:
<CODE>::<Message>. Codes: THR, CMT, AWD, COR, DOC, FLD, ANN, PNT, ASM, CLD, PMT, PLC, AUT, NTF, POL, RPT, USR, SCR. -
Soft deletes: Use
is_deletedflag orstatusenum (active/archived/deleted):thread = get_existing_thread(db, id) # Check existence only thread = get_existing_active_thread(db, id) # Check existence AND active status
-
Singleton managers:
S3Manager,SchedulerManager,PollWSManager,AIModelManagerusemetaclass=Singletonfromsrc/metaclasses.py- Example:
SchedulerManager()always returns same instance - Initialized in
src/main.pylifespan SchedulerManagerruns scheduled jobs:broadcast_poll_updates- broadcasts poll updates via WebSocketcheck_active_polls_expiration- marks expired polls as closedprocess_document_chunks- processes documents for RAG (chunking, embedding)generate_prompts_for_threads- generates AI assistant prompts for threads
- Example:
-
AI/RAG Pipeline: Retrieval-Augmented Generation for AI assistant features (requires course setting
enable_ai_assistant=True):- Document Processing (via
DocumentProcessersingleton insrc/prompt/managers.py):- Loads documents from S3 storage (supports: txt, pdf, docx, pptx)
- Splits into chunks using
RecursiveCharacterTextSplitter(default: 1000 chars, 200 overlap) - Creates embeddings via
TextEmbedder(Google Generative AI Embeddings) - Stores chunks with embeddings in
document_chunktable
- Thread Prompt Generation (via
AIModelManagersingleton):- Finds threads eligible for AI assistance (based on course settings and thread age)
- Embeds thread title + content as query vector
- Retrieves similar document chunks from same course (vector similarity search)
- Generates contextual prompt using LLM with retrieved chunks as context
- Stores prompt in
prompttable linked to thread
- Scheduled Processing:
process_document_chunks- runs every N minutes, processes documents withchunk_processing_status=PENDINGgenerate_prompts_for_threads- runs every N minutes, generates prompts for threads meeting age threshold
- Key files:
src/prompt/managers.py-DocumentProcesser,TextEmbedder,AIModelManagerclassessrc/prompt/configs.py- RAG configuration (chunk size, embedding model, prompt model)src/document/models.py-DocumentChunkmodel with embedding columnsrc/scheduler/managers.py- scheduled job definitions
- Document Processing (via
-
Base model utilities: All SQLAlchemy models inherit from
Baseinsrc/models.py:.dict()- convert model to dictionary of column values__repr__()- detailed string representation with all attributes__str__()- string representation via.dict()
thread = Thread(title="Test", content="Content") thread_dict = thread.dict() # Returns {'id': ..., 'title': 'Test', ...}
Last updated: 28-Feb-2026