Development Guide
Unified entry point for local development, code conventions, quality gates, performance and security baselines, release, and database migration governance.
This document is the unified entry point for project development, verification, performance, security, release, and database migration governance. Both automated agents and human contributors must follow these constraints.
Project change entry points
Section titled “Project change entry points”Backend
Section titled “Backend”- Data models live in
backend/models.py; structural changes must ship with a corresponding Alembic revision. - API routes go in
backend/routers/, business logic inbackend/services/, database access reusesbackend/crud.py. - New migration scripts go in
scripts/migrations/, and the migration script guide must be updated in tandem. The SQLite-to-PostgreSQL production switch steps are maintained only in the migration Runbook.
Frontend
Section titled “Frontend”- Vue components go in
frontend/src/components/, Astro pages go infrontend/src/pages/. - State management reuses Pinia stores; styling follows the existing Tailwind CSS and theme conventions.
- Frontend dependencies must be locked by
frontend/bun.lock; installation and image builds must not use npm.
Documentation entry points
Section titled “Documentation entry points”Build / Test / Run Scripts
Section titled “Build / Test / Run Scripts”Environment (required)
Section titled “Environment (required)”- Python dependency management: use uv exclusively (
uv sync/uv run); conda is no longer used. - Frontend package manager: Bun only;
npmis forbidden (do not generate or updatepackage-lock.json).
Suggested initialization (first time only):
cd backenduv syncBackend (FastAPI)
Section titled “Backend (FastAPI)”Install dependencies:
cd backenduv syncStart (development):
cd backenduv run python main.pyBackend quality gates (all must pass):
-mcheck (module execution / basic runnability)
cd backenduv run python -m compileall .- Smoke test: verify the application imports,
appexists, and key dependencies are present.
cd backenduv run python -c "import main; assert hasattr(main, 'app'); print('backend smoke: ok')"- Unit / regression tests (pytest, required): pytest is not declared in
pyproject, so use--with pytest. Tests that need a real PostgreSQL are skipped by default (gated onTEST_POSTGRES_URL).
cd backendtimeout 900 env PYTHONPATH=. uv run --with pytest pytest -q- PostgreSQL integration tests (optional; required when changing migration or data-import logic):
cd backendTEST_POSTGRES_URL='postgresql+psycopg://chronosync:<password>@localhost:5432/<test-db>' uv run --with pytest pytest tests/test_postgres_integration.py -qFrontend (Astro + Vue)
Section titled “Frontend (Astro + Vue)”Install dependencies (bun only):
cd frontendbun installStart development:
cd frontendbun run devFrontend quality gates (all must pass):
cd frontendbun run lintbun run type-checkbun run buildLint (required):
- If the repository already provides a lint config/script (e.g.
eslint/prettier), run and pass it:bun run lint/bunx eslint ./bunx prettier -c . - If the current branch has not yet introduced a lint tool: do not install dependencies via npm. Introduce lint through bun, and update both:
- the
lintscript infrontend/package.json - the lint invocation commands in this document
- the
Code Style
Section titled “Code Style”Frontend
Section titled “Frontend”- TypeScript first: avoid
any; place new type definitions in the nearest module (or reuse existing type files). - Component / file naming: Vue components use
PascalCase.vue; other files follow existing project conventions. - Minimal change: avoid unrelated refactoring; only touch what the task requires.
- Formatter: if Prettier / ESLint is adopted, follow its rules and keep output warning-free (do not sacrifice readability just to pass lint).
- Visual consistency (required): frontend component changes must follow the existing primary color palette and brand style, and cover both light and dark themes — do not make components usable in only one theme.
Backend
Section titled “Backend”- Clear layering: routes in
routers/, business logic inservices/, database access incrud.py. - Imports and side effects: avoid expensive operations at module import time; keep
main.pysafely importable. - Error handling: external APIs return a consistent structure; never expose internal exceptions or stack traces to clients.
Performance
Section titled “Performance”The following constraints derive from the current performance baseline (see “Performance conclusions and baselines” below). When touching related code, these must be followed:
- Endpoint definition (required): any endpoint that performs blocking I/O, SQLAlchemy queries / commits, password hash verification, upstream HTTP requests, or file I/O must be declared as synchronous
def(FastAPI automatically places it in the thread pool). Blocking calls insideasync defare forbidden — they block the event loop (previously, a 166ms bcrypt verification starved/healthto 168ms). Only purely asynchronous endpoints (e.g. explicitawaiton non-blocking I/O) may useasync def. - Password hashing (required): all new hashes must use argon2id with parameters
time_cost=2 / memory_cost=19456 KiB (19 MiB) / parallelism=1(_argon2_hasherinbackend/auth.py). Generating new bcrypt hashes is forbidden; existing bcrypt hashes are automatically re-hashed to argon2id on successful login (password_needs_update+authenticate_user). - Response bodies and serialization (required): schedule / event list aggregation endpoints must not return fully nested
schedule(includingclass_times) or fullowner. Personal endpoints useresponse_model_exclude={"schedule", "owner"}; team / filter endpoints use the slim modelEventTeamResponse(ScheduleBrief/UserBriefinschemas.py). Per-event lazy loading that causes N+1 (one SQL per event during serialization) is forbidden — usejoinedloadeager loading or a response model that does not trigger lazy loading. - Response timing observability (required, must be preserved):
RequestTimingMiddlewareinbackend/main.pyemitsTIMING method path status X.Xms; production uvicorn must useuvicorn_log_config.json(includes the timing field). - Performance regression gates: argon2id verification <100ms; the first successful login with a legacy bcrypt (cost 12) hash is allowed ~170ms, but must auto-upgrade to argon2id after that login. After changing an endpoint, re-test with the method described below.
Performance conclusions and baselines
Section titled “Performance conclusions and baselines”The following facts come from the current code and real measurements. Check them before touching related code to avoid regressions or duplicated investigation:
- Framework conclusion: FastAPI is sufficient for the current business scale. Existing performance bottlenecks come from blocking calls, password hashing, and large response bodies — fixes should continue to target these actual hotspots.
- Historical root cause (pre-fix baseline): single uvicorn worker (
docker/supervisord.conf--workers 1) + manyasync defendpoints performing blocking SQLAlchemy / bcrypt calls → 10 concurrentGET /api/schedule/wall time 1856ms (~10× single-request, linear queuing); login bcrypt (cost=12) verification took 166ms and stalled the event loop (during which/healthwas dragged to 168ms);GET /api/schedule/response body 857KB (590 events × nested schedule + owner). Data scale is tiny (66 users / 8464 events) — the query itself is not the bottleneck. - Post-fix baseline (regression reference, same snapshot / endpoint): argon2id verification 41ms; login endpoint 80ms;
/healthduring login 10ms; 10 concurrent heavy endpoints wall time 1134ms (remaining time is GIL contention on large-response serialization); 10 concurrent lightweight endpoints wall time 46ms (thread pool already parallel); team endpoint 3.47MB → 1.52MB (243 → 153ms); filter endpoint 349KB → 150KB; personal main path 268.6KB / 14.5ms / 4 SQL (no N+1). - Remaining bottlenecks and known optimizations: heavy endpoints (735KB–1.5MB) still contend on the GIL during pure Python serialization →
--workers 2–4enables process-level parallelism (watch for linear memory growth and connection pool sizingpool_size / worker); theeventstable lacks a(schedule_id)index and a(start_time, end_time)index (no noticeable impact at the current ~8.5k rows, but prevents degradation after data growth; index additions must go throughscripts/migrations/and update the PG integrationHEAD_REVISION); PostgreSQL can enableauto_explain(log_min_duration=100ms) to log slow queries. - Re-test method: concurrent / serial verification must carry
Authorization(responses without a token return 401 instantly, which can be misread as parallelism); event-loop starvation verification = send a slow request (e.g. login), then hit/health10ms later — pre-fix it was dragged to ~170ms, post-fix <10ms; measure hash timing directly in the auth layer (argon2id 41ms / bcrypt 166ms).
Security and Guardrails
Section titled “Security and Guardrails”- Sensitive data: never commit or echo secrets, passwords, tokens, or private URLs; avoid writing real credentials into code or documentation.
- Default accounts: the initial admin password appears only in run logs; do not hardcode weak passwords.
- Forbidden actions
npmis forbidden (includingnpm install/npm run ...).- Introducing or committing
package-lock.jsonis forbidden. - Emoji in output or commits (including documentation and comments) is forbidden — keep text professional and reviewable.
- Loosening CORS / CSP or disabling auth for development convenience is forbidden.
- Logging: avoid logging user PII (email, student ID, token, verification code, etc.); redact when needed.
Authentication and password conventions
Section titled “Authentication and password conventions”- JWT (required): HS256,
exp=30min. The token must includetoken_version(payloadtver) at issuance. Password change (/api/profile/change-password), password reset (/api/auth/reset-password), and admin password reset (the password path incrud.update_user) must all increment the user’stoken_version, invalidating old tokens immediately (get_current_user/get_optional_current_uservalidation). Any new password-change / reset path must incrementtoken_versionin sync. - Password policy (required): new passwords must be at least 8 characters (NIST SP 800-63B). Server-side
schemas.py(RegisterRequest/ResetPasswordRequest/ChangePasswordRequest) and frontend forms (RegisterForm/ForgetPasswordForm/FirstStartAdminModal, etc.) must stay in sync — do not change only one side. - Rate limiting and verification-code storage (required): login rate limiting (
login_rate_limits, default 8 / student-ID + IP / 300s, lockout 600s), registration rate limiting (register_rate_limits, default 10 / IP / 600s), and email verification codes (verification_codes, invalidated after 5 wrong attempts, constant-time comparison, single-use, 60s send cooldown) are all stored in the database and shared across workers. Reverting to in-process dicts is forbidden; any new rate-limit or verification-code state must use a DB table. Parameters can be adjusted viaAUTH_RATE_LIMIT_*/REGISTER_RATE_LIMIT_*environment variables. - Dependency ban (required): reintroducing
passlib(1.7.4 is unmaintained) is forbidden. Password hashing connects directly toargon2-cffi.PasswordHasher+bcrypt(legacy compat only); seebackend/auth.py. - Secret key (required): in production (
ENV/APP_ENV=production),SECRET_KEYmust be set — startup is refused without it; placeholder keys are rejected.
Exclude analytics before local browser testing
Section titled “Exclude analytics before local browser testing”When visiting a production or preview site from a local browser, exclude Umami analytics first to avoid counting test traffic against real data.
Open the browser developer tools Console on the target site and run:
localStorage.setItem('umami.disabled', 1)To undo:
localStorage.removeItem('umami.disabled')This setting is per-site — set it once for each domain. Agents must run the exclusion command before any browser verification.
Contribution Guidelines
Section titled “Contribution Guidelines”-
Branches and PRs
- Small, focused commits: each commit centers on one topic (fix / feature / docs).
- PR description must include: what changed, impact scope, and verification (paste the commands you ran).
- Changes touching user-facing features, frontend interactions, backend APIs, environment variables, deployment, or dev workflows must update the corresponding Markdown docs in tandem; cross-doc references must use relative links.
- After completing each feature / module / bug fix, use Conventional Commits with multiple
-mflags for a detailed commit message; describe the change points, impact scope, and verification results as an unordered list. - Commit message example:
git commit \-m "fix(frontend): improve auth form validation and toast feedback" \-m "- Replace native form blocking with toast-based validation hints." \-m "- Align OTP send-code interactions across login/register/forget flows." \-m "- Verify with bun run type-check and bun run build."
Standard contribution flow: fork the repository, create a focused feature or fix branch, commit and push following the conventions in this section, then open a Pull Request.
-
Issues
- Reproduction steps must be clear; note the environment (OS, Python version, uv version, Bun version).
- Attach the minimum necessary logs (redacted).
-
Agent collaboration flow (required)
- Before writing code, reason through the design and compare approaches; combine
web search,context7 mcp, relevantskills, official documentation, and project conventions before implementing changes. - Before editing, locate the code and existing conventions — avoid “tear it down and rewrite”.
- After writing code, run tests / verification relevant to the change (unit tests, builds, type-checks, smoke tests, etc.); do not skip verification and commit directly.
- After changes, the quality gates must be met:
- Frontend:
type-check+lint+build - Backend:
uv run python -m compileall+ smoke test +uv run --with pytest pytest -q; results are based on a full current test run.
- Frontend:
- Before writing code, reason through the design and compare approaches; combine
Version management
Section titled “Version management”When releasing a new version, the version numbers or version records in the following locations must be updated in sync:
| File | Location | Notes |
|---|---|---|
backend/pyproject.toml | version = "x.y.z" | Python project metadata, used by uv and packaging |
backend/main.py | version="x.y.z" (FastAPI init) | Version shown in API docs and on the / endpoint |
backend/main.py | "version": "x.y.z" returned by the root endpoint | GET / response body |
CHANGELOG.md | New ### vX.Y.Z (...) full entry | Complete version history |
README.md | Keep the same version entry for the most recent three months | Recent updates on the project landing page |
The frontend “changelog” modal is built from CHANGELOG.md at build time (frontend/src/layouts/DashboardLayout.astro) and only shows version entries within three months of the build date (the user-visible UI does not display this limit, so no manual version bump is needed). After changing a version number, verify: the built modal and the README’s “most recent three months” range show the same set of version entries and the same latest version; do not release if they disagree or if the latest version is missing.
CI/CD automatic behavior: pushing to main triggers GitHub Actions to read the version from backend/pyproject.toml, build a Docker image, and push it to Docker Hub with both latest and x.y.z tags. Pushing a v* git tag also appends that git tag as an additional image tag.
Manual release flow:
# 1. Update all 5 locations above (keep version numbers consistent)# 2. Commitgit commit -m "release: bump version to vX.Y.Z" -m "- ..."# 3. Taggit tag vX.Y.Z# 4. Push (triggers CI to build latest + x.y.z + vX.Y.Z image tags)git push origin main --tags- This repository allows placing a local
AGENTS.mdin subdirectories to override scoped rules; when conflicts arise, theAGENTS.mdin the nearest directory wins.
Database migration script conventions
Section titled “Database migration script conventions”- All new migration scripts must go in
scripts/migrations/; they must not be placed inbackend/migrations/. backend/migrations/no longer exists as a migration directory; legacy Alembic resources are archived underscripts/migrations/legacy_alembic/— do not place migration files back inbackend/migrations/.- Every time a migration script is added, modified, or deprecated, the following must be maintained in sync:
- Migration script guide: script list, introduced version, purpose, applicable database, usage, and caveats;
- The relevant entry and upgrade notes in the project overview, deployment guide, or full changelog.
- Before running a migration script, confirm the actual database type and connection-string source; this project currently runs on PostgreSQL, so explicitly load
backend/.envbefore running commands. - Model and migration DDL consistency (required): when adding a column or table,
models.pyand the Alembic revision must match exactly — column type, nullable, andserver_default(the model usesserver_default=text(...), the migration usesserver_default=...; both sides must have it; follow theis_default/is_hidden/token_versionprecedent). Any gap counts as schema drift. - New head sync (required): after a new revision becomes the head, update
HEAD_REVISIONinbackend/tests/test_postgres_integration.py; otherwise the CI fresh-upgrade gate check will fail. - Idempotency guards (required): table / column creation operations inside a revision must include
if not exists-style guards (seed5e6f7a8b9c0,f0a1b2c3d4e5). - Runtime state tables are excluded from the import list:
login_rate_limits/register_rate_limits/verification_codesare runtime state and must not be added toTABLES_IN_ORDERinscripts/migrations/sqlite_to_postgres.py. New tables of the same kind are excluded on the same basis.
PostgreSQL reliability verification (required after migration / data-import changes)
Section titled “PostgreSQL reliability verification (required after migration / data-import changes)”- Run
alembic upgrade headagainst a real PostgreSQL (CI usespostgres:latest) and confirm every revision applies in order andalembic_versionis a single row equal to head. - Verify zero drift between model metadata and the actual catalog: compare columns / nullable / server_default / indexes for all tables (the SQLite side is covered by
_assert_catalog_matches_metadataintest_migration_governance.py; the PG side requires manual comparison or runningTEST_POSTGRES_URLintegration tests). - Validate against an independent temporary database (do not run migration verification against production), then delete it.