FastAPI backend for the Lean In Compass product — circle matching, progressive onboarding, One Action tracking, and AI-assisted industry classification.
| Layer | Choice |
|---|---|
| Backend | FastAPI + Python 3.11 |
| Frontend | Vanilla JS SPA (no build step) |
| Database | PostgreSQL (Neon free tier recommended) |
| ORM | SQLAlchemy 2.0 async (asyncpg) |
| Migrations | Alembic |
| Validation | Pydantic v2 |
| AI | Groq (llama-3.1-8b-instant) |
| Tests | pytest |
My intro call with Ms. Griswold, CEO, shaped my approach on this project. She emphasized that Lean In’s priority is scaling users. I explored LeanIn.org and noticed the current onboarding, while clean and accessible, captures limited signal about who users are or what they need. I saw an opportunity to build on top of this by adding lightweight personalization early on.
That led to Compass. From the moment someone signs up, it gathers just enough context to guide them toward a relevant starting point, whether they are starting a circle, joining one, or exploring. It uses inputs like industry, career stage, and goals to surface matched circles and resources, so the experience feels tailored rather than generic.
Compass also supports engagement between meetings. Members commit to a One Action and share brief weekly reflections. This creates a structured but low-friction data layer that can power tools like AI-generated pre-meeting briefs for circle leaders, highlighting progress, blockers, and discussion topics. Over time, this can also provide useful signals around circle health through engagement trends.
My implementation took just over 3 hours.
Tools: I used Anthropic’s Claude for research, architecture, and backend implementation. Groq’s API, running LLaMA models, powers the industry classifier in this version. In production, I would use Claude Haiku for classification and Sonnet for summaries.
Next: Next steps would be completing the meeting summary endpoint, adding circle health scoring based on engagement, and building a proper auth layer with JWT and role-based access.
Note for reviewers: This project was built and tested against a Neon hosted Postgres database. If you don't have Docker installed, you can create a free Neon account and connect in under two minutes — no local setup required. If you do have Docker, a
docker-compose.ymlis included and works equally well. Both paths are documented below.
Option A — Neon (no Docker required, recommended for quick setup)
- Go to neon.tech and create a free account
- Create a new project — call it
compass - On the dashboard, copy the connection string (it looks like
postgresql://user:pass@ep-xxx.neon.tech/neondb) - Change
postgresql://topostgresql+asyncpg://— that's yourDATABASE_URL
Option B — Docker (local Postgres)
docker compose up -dUse this as your DATABASE_URL:
DATABASE_URL=postgresql+asyncpg://compass:compass@localhost:5432/compass
- Go to console.groq.com and sign up
- Go to API Keys and create a new key
cp .env.example .envOpen .env and fill in your values:
DATABASE_URL=postgresql+asyncpg://user:pass@ep-xxx.neon.tech/neondb
GROQ_API_KEY=your_groq_key_here
python -m venv .venv
# Mac/Linux:
source .venv/bin/activate
# Windows:
.venv\Scripts\activate
pip install -r requirements.txtalembic upgrade headpython -m app.seedYou'll see five meeting IDs printed — these can be used with POST /meetings/{id}/actions to commit new One Actions via the API.
uvicorn app.main:app --reload| Surface | URL |
|---|---|
| Frontend (SPA) | http://localhost:8000 |
| Swagger UI | http://localhost:8000/docs |
| ReDoc | http://localhost:8000/redoc |
All accounts use passphrase compass.
| What it demonstrates | |
|---|---|
sarah.chen@example.com |
5-week engagement streak · in-progress One Action |
maria.rodriguez@example.com |
3-week streak · committed action with reflections |
jennifer.kim@example.com |
Finance Power Circle · committed negotiation action |
alex.thompson@example.com |
Incomplete profile (red dot) · Campus Connect |
lisa.park@example.com |
Two circles (Campus Connect + Nonprofit Warriors) · two committed actions |
priya.patel@example.com |
0-week streak · committed action — ideal for the 0→1 reflection demo |
Onboarding flow
- Click Get Started on the landing page and complete the three-step onboarding
- After finishing, the dashboard defaults to the explore view — click Find a Circle to Join or Start My Own Circle to set your intent
Matching & joining
- Log in as
sarah.chen@example.com(passphrase:compass) - Go to Find Matches — see ranked circles with score breakdowns
- Join a circle — it immediately appears in My Circles on the Profile tab
One Action & reflections
- Log in as
priya.patel@example.com - Go to the Profile tab → My Circles — she has a committed action with zero reflections
- Click + Add reflection and submit one — her streak jumps from 0 to 1 week
- Post a second reflection in a later week to see the streak grow
Committing a new One Action
- Join any circle that doesn't already have an active action for your account
- The circle card shows + Commit a One Action — click it, fill in the description and due date
- The action immediately appears with tracking controls
Profile completeness
- Log in as
alex.thompson@example.com— profile is incomplete (red dot in navbar) - Go to the Profile tab and fill in the missing fields — the score updates in real time
pytest tests/ -vThe matching algorithm tests are pure unit tests — no database or API key needed.
| Method | Path | Description |
|---|---|---|
POST |
/onboarding/start |
Create user + profile (step 1) |
PATCH |
/users/{id}/profile |
Update profile fields, recalculate completeness |
GET |
/users/{id}/profile-completeness |
Get score + missing fields (drives "red dot") |
| Method | Path | Description |
|---|---|---|
GET |
/users/{id}/dashboard |
Personalised dashboard, branches on intent |
| Method | Path | Description |
|---|---|---|
POST |
/auth/login |
Authenticate with email + passphrase |
| Method | Path | Description |
|---|---|---|
GET |
/circles |
List circles with optional filters |
GET |
/users/{id}/circle-matches |
Ranked matches with score breakdown |
GET |
/users/{id}/circles |
Circles the user belongs to, with active One Action + engagement streak |
POST |
/circles/{id}/join-requests |
Join a circle |
| Method | Path | Description |
|---|---|---|
POST |
/meetings/{id}/actions |
Commit a One Action after a meeting |
GET |
/actions/{id} |
Get a single action with all updates |
PATCH |
/actions/{id} |
Update status or add a completion note |
POST |
/actions/{id}/updates |
Post a free-text progress update |
POST |
/actions/{id}/reflections |
Post a structured weekly reflection (progress, what worked, challenge, next step, confidence 1–5) |
Located in app/services/matching.py. Deterministic and explainable — no ML.
total_score = (
goal_overlap_score * w1 + # fraction of user goals covered by circle
career_stage_score * w2 + # proximity of user stage to member distribution
industry_score * w3 + # user industry vs circle industry focus
format_match_score * w4 # virtual / in-person / hybrid alignment
)
Weights w1–w4 are personalised per user via importance toggles (low / medium / high) set on their profile. Unset dimensions fall back to algorithm defaults. Weights are always normalised to sum to 1.0.
spots_remaining is returned as a separate informational field and does not affect ranking.
Located in app/services/completeness.py.
| Field | Points |
|---|---|
| intent | 20 |
| career_stage | 15 |
| industry | 15 |
| goals (all 3) | 20 |
| location | 10 |
| open_to_networking | 10 |
| linkedin_url | 10 |
| Total | 100 |
is_complete = score >= 80. This drives the red-dot UI indicator.
Located in app/services/engagement.py.
A streak counts consecutive ISO weeks (Monday–Sunday) where the user posted at least one reflection or action update. Starting from the most recent week with activity and counting backwards — the current week is not penalised if it hasn't been posted to yet.
streak = 0
anchor = most-recent week with an update
while anchor in weeks_with_updates:
streak += 1
anchor = previous week
The streak is returned on GET /users/{id}/circles and displayed on the profile page next to each circle.
POST /actions/{id}/reflections accepts:
{
"user_id": "...",
"progress": "What I did this week toward my action",
"what_worked": "Optional — what went well",
"challenge": "Optional — what was hard",
"next_step": "What I'll do before next week",
"confidence": 4
}These are formatted into a human-readable update_text and stored in action_updates. The format is deliberately structured so the planned AI summary endpoint can ingest individual reflections and produce richer circle-health summaries (see "What I'd build with more time").
After each meeting, a circle leader should be able to generate a structured summary of all members' One Actions and reflections — wins, blockers, confidence trends, and suggested discussion topics for the next session.
The structured reflection format (--- Weekly Reflection --- / Progress / What worked / Challenge / Next step / Confidence) is deliberately machine-readable so a prompt can ingest it directly. The GROQ_API_KEY environment variable is already wired into the app config.
The implementation would be:
- A
POST /meetings/{id}/summary/generateendpoint that aggregates all actions + reflection updates for the meeting, sends them to Groq (llama-3.3-70b-versatile), and stores the structured result - A
GET /meetings/{id}/summaryendpoint to retrieve the stored summary - A Meeting Summary tab inside each circle card — circle leader taps "Generate summary" after the meeting, result is visible to all members
Status: Not yet implemented — designed for the next milestone.
The structured reflection format is intentionally built to feed an automated circle-health pipeline:
- Aggregate reflections across all circle members between meetings.
- Send to Groq (
llama-3.3-70b-versatile) with a prompt that synthesises themes, blockers, confidence trends, and standout wins. - Return to the circle leader a pre-meeting brief: what the group has been working on, common challenges, suggested discussion topics, and momentum indicators.
This extends the planned POST /meetings/{id}/summary/generate endpoint described above — the same action aggregation pipeline would include per-reflection themes and produce a proactive brief before the meeting rather than after.
These are fully implemented on the backend and exist intentionally — I built them to support future milestones but didn't reach them on the frontend within the scope of this project.
| Feature | Where it lives | Why it's there |
|---|---|---|
GET /actions/{action_id} |
app/routers/actions.py |
Fetching a single action with all its updates. Supports a future action-detail page or deep-link from notifications. |
POST /actions/{id}/updates (plain text) |
app/routers/actions.py |
Free-text progress update, separate from structured reflections. Reflections are the preferred path; plain updates remain for lightweight check-ins. |
GET /circles (with filters) |
app/routers/circles.py |
Now exposed via the Circle Directory page. |
ActionStatusEnum.missed |
app/models/enums.py |
Reserved for a scheduled task that auto-transitions overdue actions. No Celery worker yet. |
Meeting.curriculum_topic, Meeting.notes |
app/models/meeting.py |
Fields for structured meeting agendas. No create-meeting endpoint exists yet; populated via seed only. |
CircleMemberRoleEnum.leader |
app/models/circle.py |
Role is stored and returned but not yet used for authorization — all members can currently take the same actions. Role-gating is a production prerequisite. |
UserProfile.onboarding_step |
app/models/user.py |
Incremented on each profile update. Intended to drive a resume-onboarding flow (e.g. deep-link sends user back to the right step). Frontend navigates by view state instead. |
JoinRequestIn.message |
app/schemas/circle.py |
Accepted in the join request body and echoed in the response, but not stored — the stub goes straight to membership. The production path stores it in a pending-requests table for leader review. |
| Feature | Current stub | Production path |
|---|---|---|
| Authentication | Email + plaintext passphrase | JWT + OAuth2 (e.g. Auth0); bcrypt for passphrase |
| Authorization | No per-request user check | Middleware that verifies the token's sub matches the user_id in the path |
| Join requests | Direct membership creation | Pending-request table + leader notification email |
| Action reminders | Not implemented | Celery + Redis scheduled task to mark overdue actions missed |
| Dashboard content | Hardcoded in router | CMS (Contentful / Sanity) |
| CORS origins | * |
Restrict to Lean In frontend domain |
| Circle creation | No endpoint | Full CRUD for circles with leader assignment |
| Pagination | No limits on list endpoints | limit / offset query params on /circles, /users/{id}/circles, action updates |
| Circle health AI | Not implemented | See "Planned" section above |
compass-api/
├── app/
│ ├── main.py # FastAPI app + router registration
│ ├── config.py # pydantic-settings env config
│ ├── database.py # Async engine + session factory
│ ├── models/
│ │ ├── enums.py # All shared enum definitions
│ │ ├── user.py # User, UserProfile, UserGoal
│ │ ├── circle.py # Circle, CircleMember, CircleFocus
│ │ ├── meeting.py # Meeting
│ │ └── action.py # Action, ActionUpdate
│ ├── schemas/ # Pydantic v2 request/response models
│ ├── routers/ # FastAPI route handlers
│ ├── services/
│ │ ├── matching.py # Pure scoring functions + DB entry point
│ │ ├── completeness.py # Profile completeness scorer
│ │ └── industry.py # Groq: classify free-text industry at write time
│ └── seed.py # Realistic demo data
├── alembic/ # Database migrations
├── tests/
│ └── test_matching.py # Matching algorithm unit tests
├── docker-compose.yml # Optional local Postgres
├── requirements.txt
└── .env.example