Production-style JWT auth with access + refresh tokens, refresh rotation, bcrypt password hashing, and protected routes.
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtCreate .env (example included) and start the server from the repo root:
uvicorn app.main:app --reloadIf you run commands from backend/ instead, use:
uvicorn main:app --reloadFor private media attachments via Supabase Storage, also set:
SUPABASE_URL=https://<project-ref>.supabase.co
SUPABASE_KEY=<service-role-or-anon-key-with-storage-policies>
SUPABASE_BUCKET=chat-media
SUPABASE_SIGNED_URL_EXPIRES_SECONDS=3600To enable at-rest chat message encryption, set a Fernet key:
MESSAGE_ENCRYPTION_KEY=<base64-fernet-key>Generate one with:
python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"For key rotation, you can use:
MESSAGE_ENCRYPTION_KEYS=<newest-key>,<older-key>,<older-key-2>For S3-compatible storage (AWS S3 / Supabase S3 endpoint / MinIO), set:
S3_ENDPOINT_URL=https://<endpoint>
S3_ACCESS_KEY_ID=<access-key-id>
S3_SECRET_ACCESS_KEY=<secret-access-key>
S3_REGION=ap-south-1
S3_BUCKET=chat-media
S3_PRESIGNED_URL_EXPIRES_SECONDS=3600If S3 vars are set, backend uses S3; otherwise it falls back to Supabase API keys.
Google SSO uses this client ID by default:
GOOGLE_CLIENT_ID=177857212507-6lvdpulpk5f0jtem3g2ttp5ekb2k7l6n.apps.googleusercontent.comPOST /auth/registerPOST /auth/loginPOST /auth/googlePOST /auth/refreshGET /users/mePATCH /users/mePOST /users/me/profile-image/upload-urlPOST /api/stories/upload-urlPOST /api/storiesGET /api/stories/feedGET /api/stories/{userId}POST /api/stories/{storyId}/viewDELETE /api/stories/{storyId}GET /auth/me(protected)GET /messages/attachments/{attachment_id}(protected redirect to signed URL)GET /messages/attachments/signed-url?attachment_id=<path>(protected direct signed URL response)
curl -X POST http://localhost:8000/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"Passw0rd!"}'
curl -X POST http://localhost:8000/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"Passw0rd!"}'
curl -X POST http://localhost:8000/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refresh_token":"<refresh-token>"}'
curl http://localhost:8000/auth/me \
-H "Authorization: Bearer <access-token>"
curl http://localhost:8000/users/me \
-H "Authorization: Bearer <access-token>"Frontend can use Google's official script and send the returned credential JWT to the backend:
<script src="https://accounts.google.com/gsi/client" async defer></script>
<div
id="g_id_onload"
data-client_id="177857212507-6lvdpulpk5f0jtem3g2ttp5ekb2k7l6n.apps.googleusercontent.com"
data-callback="handleCredentialResponse">
</div>
<div class="g_id_signin"></div>
<script>
async function handleCredentialResponse(response) {
const authResponse = await fetch("http://localhost:8000/auth/google", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ credential: response.credential }),
});
console.log(await authResponse.json());
}
</script>The backend verifies the Google token against GOOGLE_CLIENT_ID, creates the user if it does not exist yet, and then returns the same local access and refresh tokens used by the standard login flow.
Stories are implemented in this FastAPI + MongoDB backend and reuse the existing auth and friendship graph.
Behavior:
- Story media is stored in S3 via presigned upload URLs.
- Story metadata lives in MongoDB collections
storiesandstory_views. - Only the story owner and accepted friends can see active stories.
- Stories expire after
STORY_TTL_HOURSand a background cleanup loop deletes expired media from S3 and soft-deletes the database rows. - Feed responses are grouped by user, sorted by newest active story first, and mark whether the viewer has already seen each story.
Environment:
AWS_ACCESS_KEY_ID=<access-key>
AWS_SECRET_ACCESS_KEY=<secret-key>
AWS_REGION=ap-south-1
S3_STORY_BUCKET=chat-stories
MONGODB_URI=<mongo-connection-string>
JWT_SECRET_KEY=<existing-access-secret>
JWT_REFRESH_SECRET_KEY=<existing-refresh-secret>API:
POST /api/stories/upload-url
{
"fileName": "photo.jpg",
"mimeType": "image/jpeg",
"fileSize": 123456
}POST /api/stories
{
"s3Key": "stories/user_example.com/uuid-photo.jpg",
"bucket": "chat-stories",
"mimeType": "image/jpeg",
"fileSize": 123456,
"originalFileName": "photo.jpg",
"caption": ""
}GET /api/stories/feed
- Returns the current user and accepted friends that have active stories.
GET /api/stories/{userId}
- Returns one user's active stories if the requester is the owner or an accepted friend.
POST /api/stories/{storyId}/view
- Marks a story as viewed for the current user without creating duplicate view rows.
DELETE /api/stories/{storyId}
- Owner only. Deletes the S3 object and soft-deletes the story row.
- Response:
{
"status": "deleted",
"storyId": "story-123"
}Realtime updates:
- Connect to
ws://<host>/ws/chat?token=<access-token>. - Story changes now emit websocket packets to the story owner and all accepted friends.
- Upload event:
{
"type": "story",
"event": "created",
"owner_id": "owner@example.com",
"story_id": "story-123",
"story": {
"id": "story-123",
"userId": "owner@example.com",
"createdAt": "2026-03-30T10:00:00+00:00",
"expiresAt": "2026-03-31T10:00:00+00:00"
}
}- Delete event:
{
"type": "story",
"event": "deleted",
"owner_id": "owner@example.com",
"story_id": "story-123"
}Profile editing uses the same S3 presigned upload approach as stories.
Environment:
AWS_ACCESS_KEY_ID=<access-key>
AWS_SECRET_ACCESS_KEY=<secret-key>
AWS_REGION=ap-south-1
S3_BUCKET=chat-media
S3_PROFILE_BUCKET=chat-profiles
PROFILE_IMAGE_MAX_UPLOAD_SIZE_BYTES=5242880API:
GET /users/me
- Returns the current user's profile.
POST /users/me/profile-image/upload-url
{
"fileName": "avatar.png",
"mimeType": "image/png",
"fileSize": 204800
}PATCH /users/me
{
"name": "Suryaa",
"profileImage": {
"bucket": "chat-profiles",
"s3Key": "profiles/user_example.com/uuid-avatar.png",
"mimeType": "image/png",
"fileSize": 204800,
"originalFileName": "avatar.png"
}
}To remove the avatar:
{
"removeProfileImage": true
}- Refresh tokens are stored hashed in MongoDB and rotated on every refresh.
- Access and refresh tokens use separate secrets.
- Message attachments are stored in Supabase Storage; MongoDB stores attachment metadata including bucket object path (
attachment.file_id).