{"id":50695739,"url":"https://github.com/hngprojects/meetmind-api","last_synced_at":"2026-06-09T06:08:30.891Z","repository":{"id":358299946,"uuid":"1238225557","full_name":"hngprojects/meetmind-api","owner":"hngprojects","description":null,"archived":false,"fork":false,"pushed_at":"2026-05-31T02:55:37.000Z","size":4872,"stargazers_count":1,"open_issues_count":1,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-31T04:19:37.095Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hngprojects.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"security/vulnerability-exceptions.json","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-13T23:49:29.000Z","updated_at":"2026-05-24T15:22:16.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/hngprojects/meetmind-api","commit_stats":null,"previous_names":["hngprojects/meetmind-api"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/hngprojects/meetmind-api","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hngprojects%2Fmeetmind-api","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hngprojects%2Fmeetmind-api/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hngprojects%2Fmeetmind-api/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hngprojects%2Fmeetmind-api/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hngprojects","download_url":"https://codeload.github.com/hngprojects/meetmind-api/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hngprojects%2Fmeetmind-api/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34093840,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-09T02:00:06.510Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-09T06:08:30.081Z","updated_at":"2026-06-09T06:08:30.881Z","avatar_url":"https://github.com/hngprojects.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# MeetMind Backend\n\nAI-powered meeting intelligence platform. The backend manages users, workspaces, meetings, interviews, integrations, and an \"Ask Mind\" conversational Q\u0026A layer over meeting transcripts.\n\n---\n\n## Stack\n\n| Layer | Choice |\n|---|---|\n| Web framework | FastAPI (`fastapi[standard]`) |\n| Server | Uvicorn (via `fastapi dev` / `fastapi run`) |\n| ORM | SQLAlchemy 2.0 (async) |\n| DB driver | `asyncpg` (Postgres) |\n| Migrations | Alembic (async-aware) |\n| Auth | JWT via `python-jose` + bcrypt password hashing |\n| Config | `pydantic-settings` (reads `.env`) |\n| Package manager | `uv` |\n| Tests | `pytest` + `pytest-asyncio` + `httpx.AsyncClient` |\n| Python | 3.13+ |\n\n---\n\n## Project structure\n\n```\nmeetmind-be/\n├── app/\n│   ├── main.py                        # FastAPI app, global exception handlers\n│   ├── core/\n│   │   ├── config.py                  # Settings loaded from .env\n│   │   ├── exceptions.py              # Domain exception hierarchy\n│   │   └── responses.py               # Standardized response envelope (success / error)\n│   ├── api/\n│   │   ├── deps.py                    # Shared dependencies: DBSession, CurrentUser\n│   │   └── v1/\n│   │       ├── router.py              # Aggregates all v1 domain routers\n│   │       └── routes/\n│   │           ├── health.py          # GET /health\n│   │           ├── auth.py            # POST /auth/signup, /verify-email, /resend-verification\n│   │           ├── users.py           # (stub) /users/*\n│   │           ├── workspaces.py      # (stub) /workspaces/*\n│   │           ├── meetings.py        # (stub) /meetings/*\n│   │           ├── interviews.py      # (stub) /interviews/*\n│   │           ├── integrations.py    # (stub) /integrations/*\n│   │           └── ask_mind.py        # (stub) /ask-mind/*\n│   ├── db/\n│   │   └── session.py                 # Async engine + session factory + get_session()\n│   ├── models/\n│   │   ├── base.py                    # DeclarativeBase, UUIDPrimaryKey (v7), TimestampMixin\n│   │   ├── user.py                    # User, RefreshToken, SSOProvider, ActiveSession, preferences...\n│   │   ├── workspace.py               # Workspace, WorkspaceMember, WorkspaceInvite\n│   │   ├── meeting.py                 # Meeting, MeetingParticipant, MeetingComment\n│   │   ├── transcript.py              # Transcript, TranscriptSegment, MeetingSummary, ActionItem...\n│   │   ├── interview.py               # Candidate, Interview, InterviewTranscript, InterviewSummary...\n│   │   ├── scorecard.py               # ScorecardCategory, InterviewScorecard, ScorecardScore...\n│   │   ├── integration.py             # UserPlatformIntegration, Integration, IntegrationChannel...\n│   │   ├── email_verification.py      # EmailVerificationToken\n│   │   └── ask_mind.py                # AskMindSession, AskMindMessage, AskMindSuggestedPrompt\n│   ├── schemas/\n│   │   ├── auth.py                    # SignupRequest\n│   │   └── verification.py            # VerifyEmailRequest, ResendVerificationRequest\n│   └── services/\n│       ├── auth.py                    # AuthService: hashing, user creation, JWT issuance\n│       └── verification_service.py    # VerificationService: token lifecycle\n├── alembic/\n│   ├── env.py                         # Wired to app.models.Base.metadata + settings\n│   └── versions/                      # Migration files\n├── tests/\n│   ├── conftest.py                    # In-memory SQLite test DB, AsyncClient fixture\n│   ├── test_auth.py\n│   ├── test_health.py\n│   ├── test_models.py\n│   ├── test_verification.py\n│   └── test_verification_api.py\n├── docs/\n│   └── architecture/\n│       └── system-overview.md         # Mermaid architecture diagram\n├── .env.example\n├── alembic.ini\n├── pyproject.toml\n└── uv.lock\n```\n\n### Why this layout\n\n- **`core/responses.py`** — single envelope for every response in the codebase. Clients always get `{success, message, data}` or `{success, message, error}`. No surprises.\n- **`api/deps.py`** — all shared FastAPI dependencies live here. Routes import `DBSession` and `CurrentUser` from one place.\n- **`models` / `schemas` / `services` split** — DB shape, API shape, and business logic stay decoupled. They diverge sooner than you'd think.\n- **`db/session.py` separate from `models/`** — engine setup is infrastructure; models are domain.\n- **UUID v7 primary keys** — time-ordered, so rows sort by insertion order naturally and index locality is preserved.\n\n---\n\n## Getting started\n\n### 1. Prerequisites\n\n- Python 3.13+\n- [uv](https://docs.astral.sh/uv/) (`curl -LsSf https://astral.sh/uv/install.sh | sh`)\n- A running Postgres instance (local, Docker, or Supabase)\n\n### 2. Install\n\n```bash\nuv sync\n```\n\n### 3. Configure\n\n```bash\ncp .env.example .env\n```\n\nFill in `.env`:\n\n```env\nDATABASE_URL=postgresql+asyncpg://user:password@host:5432/dbname\n\nJWT_SECRET=\u003cgenerate: python3 -c \"import secrets; print(secrets.token_hex(32))\"\u003e\nJWT_ALGORITHM=HS256\nACCESS_TOKEN_EXPIRE_MINUTES=30\nREFRESH_TOKEN_EXPIRE_MINUTES=10080\n```\n\n`pydantic-settings` will fail loudly at startup if any required key is missing.\n\n### 4. Run migrations\n\n```bash\nuv run alembic upgrade head\n```\n\n### 5. Start the dev server\n\n```bash\nuv run fastapi dev app/main.py\n```\n\n- Root → `http://127.0.0.1:8000`\n- Health → `http://127.0.0.1:8000/api/v1/health`\n- Swagger UI → `http://127.0.0.1:8000/docs`\n- ReDoc → `http://127.0.0.1:8000/redoc`\n\n---\n\n## API overview\n\nAll responses use a standardized envelope defined in `app/core/responses.py`.\n\n**Success**\n```json\n{ \"success\": true, \"message\": \"...\", \"data\": {}, \"meta\": null }\n```\n\n**Error**\n```json\n{ \"success\": false, \"message\": \"...\", \"error\": { \"code\": \"snake_case_code\", \"details\": null } }\n```\n\n### Live endpoints\n\n| Method | Path | Auth | Description |\n|--------|------|------|-------------|\n| `GET` | `/` | No | Root liveness check |\n| `GET` | `/api/v1/health` | No | DB connectivity probe |\n| `POST` | `/api/v1/auth/signup` | No | Register user, issue JWT + refresh token |\n| `POST` | `/api/v1/auth/verify-email` | No | Redeem single-use email verification token |\n| `POST` | `/api/v1/auth/resend-verification` | No | Issue a fresh verification token |\n\n### Stub routers (registered, no endpoints yet)\n\n`/api/v1/users`, `/api/v1/workspaces`, `/api/v1/meetings`, `/api/v1/interviews`, `/api/v1/integrations`, `/api/v1/ask-mind`\n\n---\n\n## Authentication\n\n### How tokens are issued\n\n`POST /api/v1/auth/signup` returns both tokens in the response body and sets them as `httponly; secure; samesite=lax` cookies:\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"...\",\n    \"email\": \"user@example.com\",\n    \"access_token\": \"\u003cjwt\u003e\",\n    \"refresh_token\": \"\u003copaque\u003e\"\n  }\n}\n```\n\nThe access token is a signed JWT (HS256). The refresh token is an opaque random string — only its SHA-256 hash is stored in the database.\n\n### How to protect a route\n\nImport `CurrentUser` from `app.api.deps` and add it to the route signature. FastAPI resolves it automatically — no decorator, no middleware.\n\n```python\nfrom app.api.deps import CurrentUser\nfrom app.core.responses import success\n\n@router.get(\"/me\")\nasync def get_me(user: CurrentUser):\n    return success({\"id\": str(user.id), \"email\": user.email})\n```\n\n`CurrentUser` is defined as:\n\n```python\nCurrentUser = Annotated[User, Depends(get_current_user)]\n```\n\n`get_current_user` accepts the token from either:\n- The `access_token` httponly cookie (sent automatically by the browser after login)\n- An `Authorization: Bearer \u003ctoken\u003e` header (for API clients / Postman)\n\nCookie takes priority. If neither is present or the token is invalid/expired, it raises a `401` error envelope automatically.\n\n### Public vs protected at a glance\n\n```python\n# Public — no auth dependency\n@router.get(\"/health\")\nasync def health(db: DBSession): ...\n\n# Protected — 401 if token missing or invalid\n@router.get(\"/me\")\nasync def get_me(user: CurrentUser): ...\n\n# Protected + DB access\n@router.get(\"/profile\")\nasync def get_profile(user: CurrentUser, db: DBSession): ...\n```\n\n### Adding extra guards\n\nLayer additional dependencies if needed:\n\n```python\nasync def require_verified(user: CurrentUser) -\u003e User:\n    if not user.is_verified:\n        raise APIError(\"Email not verified\", status_code=403, code=\"unverified\")\n    return user\n\nVerifiedUser = Annotated[User, Depends(require_verified)]\n```\n\n---\n\n## Running tests\n\nTests use an in-memory SQLite database — no external DB needed.\n\n```bash\nuv run pytest\n```\n\n`pytest-asyncio` is set to `auto` mode so async tests need no decorator. The `conftest.py` wires up an `AsyncClient` via `ASGITransport` and overrides the `get_session` dependency with a test-scoped SQLite session.\n\n---\n\n## Migrations workflow\n\n### Current migration chain\n\n```\ncacd5554ba5a  create all tables (base)\n├── d44e91e81013  add refresh token table\n│   └── 8d114ef61fcc  make refresh token datetimes timezone-aware\n├── a8cbb47717b3  add refresh_token_hash to active_sessions\n│   └── 864df66fbeb7  add unique constraint on refresh_token_hash\n└── 64a8c4b4d071  add email verification tokens table\n5b25f8695307  merge all heads\n7ba4c8acd02e  sync schema with models (is_verified + missing timestamps)\n2cdac93874a4  make email_verification_token datetimes timezone-aware  ← HEAD\n```\n\n### Typical cycle\n\n```bash\n# 1. Edit a model in app/models/\n# 2. Generate a migration\nuv run alembic revision --autogenerate -m \"describe the change\"\n# 3. Review the generated file carefully before applying\n# 4. Apply\nuv run alembic upgrade head\n```\n\n### Important: add new models to `app/models/__init__.py`\n\nAlembic discovers models by importing them. If a model file is not imported in `__init__.py`, Alembic won't see it and will either miss the table entirely or, worse, detect it as a table to drop.\n\n```python\n# app/models/__init__.py — every model must be listed here\nfrom app.models.my_new_model import MyNewModel\n```\n\n### Gotchas encountered in this project\n\n**Multiple heads** — branched migrations leave Alembic with multiple `HEAD` revisions. Before running `upgrade head`, merge them first:\n```bash\nuv run alembic merge heads -m \"merge all heads\"\nuv run alembic upgrade head\n```\n\n**Duplicate columns in branched migrations** — if two branches both autogenerated against the same base, they'll each try to add the same columns. Remove the duplicates from the later branch manually before applying.\n\n**`NOT NULL` column on existing rows** — autogenerate emits `nullable=False` without a server default, which fails if the table has data. Add `server_default` to the migration:\n```python\n# Before applying, change this:\nop.add_column('users', sa.Column('is_verified', sa.Boolean(), nullable=False))\n# To this:\nop.add_column('users', sa.Column('is_verified', sa.Boolean(), server_default=sa.text('false'), nullable=False))\n```\n\n**Timezone mismatch** — `DateTime` columns (naive) reject timezone-aware datetimes from Python. Use `DateTime(timezone=True)` on any column that stores a UTC timestamp, and regenerate the migration.\n\n### Useful commands\n\n| Command | What it does |\n|---|---|\n| `alembic upgrade head` | Apply all pending migrations |\n| `alembic downgrade -1` | Roll back one migration |\n| `alembic current` | Show applied revision |\n| `alembic history` | Full migration chain |\n| `alembic merge heads -m \"msg\"` | Merge diverged heads into one |\n| `alembic downgrade base` | Wipe everything (dev only) |\n\n### Rules\n\n- Always **review** the autogenerated file before applying — Alembic misses enum changes, some index renames, and server-side defaults.\n- **Never edit** a migration that has been applied to a shared environment. Write a new one.\n- Run `alembic upgrade head` **during deploy**, not at app startup.\n\n---\n\n## Adding new code\n\n### New endpoint\n\n1. Add route handlers to the relevant file in `app/api/v1/routes/`\n2. Import and register in `app/api/v1/router.py` if it's a new domain (already done for existing stubs)\n\n### New model\n\n1. Create or extend a file in `app/models/`\n2. Subclass `Base` (+ `UUIDPrimaryKey`, `TimestampMixin` as needed)\n3. Import the model in `app/models/__init__.py` so Alembic discovers it\n4. Generate and apply a migration\n\n### New schema\n\nAdd Pydantic request/response models to `app/schemas/`. Keep them separate from ORM models — the API shape and DB shape diverge quickly.\n\n### New business logic\n\nAdd it to `app/services/`. Routes should stay thin: validate input → call service → return envelope.\n\n### New setting\n\nAdd the field to `Settings` in `app/core/config.py` and to `.env.example`. The app fails loudly at startup if the value is missing.\n\n---\n\n## Conventions\n\n- **Absolute imports only** (`from app.core.config import settings`), never relative.\n- **Type hints everywhere** — FastAPI uses them for validation and OpenAPI generation.\n- **Routes return `success()` / `paginated()`**, never raw dicts or ORM objects.\n- **Raise `APIError`** for all domain errors — the global handler converts them to the error envelope.\n- **`async def` for anything that touches I/O** (DB, HTTP, files). Sync `def` is fine for pure CPU work.\n\n---\n\n## Production checklist\n\n- Replace `fastapi dev` with `fastapi run` (or `uvicorn app.main:app --workers N`)\n- Run `alembic upgrade head` as a deploy step before new instances boot\n- Configure DB pool size to match your worker count\n- Add CORS and request logging middleware in `app/main.py`\n- Store secrets in your platform's secret manager, not in `.env` files\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhngprojects%2Fmeetmind-api","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhngprojects%2Fmeetmind-api","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhngprojects%2Fmeetmind-api/lists"}