Profile API
A profile is the dating-side view of a user: name, bio, demographics, preferences. Authentication identity (email, password, etc.) lives in Supabase Auth and is not part of the profile.
Status
| Endpoint | Status |
|---|---|
POST /profiles | ✅ Shipped |
GET /profiles | ✅ Shipped |
GET /profiles/:id | ✅ Shipped |
PATCH /profiles | ✅ Shipped |
DELETE /profiles | ✅ Shipped |
| Profile educations | ✅ Shipped |
| Profile languages | ✅ Shipped |
| Profile photos / media | ✅ Shipped (private Supabase Storage) |
| Username, bulk lookup, onboarding | ✅ Shipped |
Source:
src/feats/profiles/.
Get own profile
Fetches the profile for the currently authenticated user, including educations and languages.
Request
GET /profiles
Authorization: Bearer <supabase-access-token>
Response
200 OK with the profile object including educations and languages
arrays.
Errors
| Status | Cause |
|---|---|
401 | Missing / invalid bearer token |
404 | Profile not found |
Get profile by ID
Fetches any profile by its UUID.
Request
GET /profiles/:id
Authorization: Bearer <supabase-access-token>
Response
200 OK with the profile object including educations and languages.
Errors
| Status | Cause |
|---|---|
401 | Missing / invalid bearer token |
404 | Profile not found |
Create profile
Creates the dating profile for the currently authenticated Supabase user. Only
firstName is required at this stage; the survey can be filled in later with
PATCH. The user's id is taken from the verified JWT, so clients do not send it.
Request
POST /profiles
Authorization: Bearer <supabase-access-token>
Content-Type: application/json
{
"firstName": "Nurbek",
"bio": "I am very cool",
"birthdate": "2004-12-05",
"height": 193,
"hometown": "Almaty",
"datingPurpose": "friendship",
"showDatingPurpose": true,
"religion": "atheism",
"familyPlan": "no-maybe-in-future",
"financialState": "wealthy",
"bodyType": "fit",
"alcoholAttitude": "negative",
"smokingAttitude": "negative",
"educations": [
{ "level": "bachelor", "inProgress": true }
],
"languages": ["kk", "ru", "en"]
}
Fields
| Field | Type | Required | Description |
|---|---|---|---|
firstName | string | ✅ | Display name |
| Other survey fields | mixed | Optional during creation; required to complete onboarding | |
showDatingPurpose, showZodiac | boolean | Privacy toggles, default false | |
educations, languages | arrays | Default to empty arrays | |
city, sex, latitude, longitude, isVisibleOnMap | mixed | Current location and discovery fields |
The Zod schema is
profiles.schemas.ts;
the corresponding Drizzle table is
profile.schema.ts.
Server-derived fields
These are computed by the API, not sent by the client:
| Field | Source |
|---|---|
userId | Supabase user from the JWT |
zodiac | Derived from birthdate (getZodiacSign(date)) |
Response
201 Created with the complete own-profile shape, including isCompleted,
missingFields, legal-term state, photos, and privacy-filtered fields.
Errors
Validation errors are returned by the global error handler in
src/core/server.ts:
{
"error": "Validation failed",
"fields": [
{ "field": "height", "message": "Number must be greater than or equal to 55" }
]
}
| Status | Cause |
|---|---|
400 | Validation failed (see body) |
401 | Missing / invalid bearer token |
500 | Internal error |
Update profile
Partially updates the authenticated user's profile. Only provided fields are updated.
Request
PATCH /profiles
Authorization: Bearer <supabase-access-token>
Content-Type: application/json
{
"bio": "Updated bio",
"height": 190
}
All fields from CreateProfileSchema are optional in the update. If
birthdate is updated, zodiac is recalculated automatically.
Response
200 OK with the updated profile.
Errors
| Status | Cause |
|---|---|
400 | Validation failed |
401 | Missing / invalid bearer token |
404 | Profile not found |
Delete profile
Permanently deletes the authenticated user's profile.
Request
DELETE /profiles
Authorization: Bearer <supabase-access-token>
Response
{ "deleted": true }
Errors
| Status | Cause |
|---|---|
401 | Missing / invalid bearer token |
404 | Profile not found |
Enumerations
These mirror the constants in
profiles.constants.ts.
Some are also seeded into reference tables for joins.
datingPurpose
dating, relationship, friendship, chatting
bodyType
slim, normal, fit, sportive, muscular, stocky, chubby
familyPlan
no-dont-want, no-not-soon, no-maybe-in-future, no-but-want-to,
already-have
financialState
unemployed, studying, irregular-income, low-income,
middle-income, wealthy
religion
catholicism, orthodoxy, islam, protestantism, buddhism,
hinduism, atheism, agnosticism, judaism
attitude (alcoholAttitude, smokingAttitude)
positive, neutral, negative — backed by the Postgres enum
attitude.
zodiac (server-derived)
aquarius, pisces, aries, taurus, gemini, cancer, leo,
virgo, libra, scorpio, sagittarius, capricorn — backed by the
Postgres enum zodiac.
educationLevel
basic_general, average_general, secondary_special, bachelor,
specialty, master, postgraduate — backed by the Postgres enum
education_level. Used by the profile_educations table.
Example with curl
ACCESS=$(bun run ./devtools/authenticate.ts)
curl -X POST http://localhost:8080/profiles \
-H "Authorization: Bearer $ACCESS" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Nurbek",
"bio": "I am very cool",
"birthdate": "2004-12-05",
"height": 193,
"hometown": "Almaty",
"datingPurpose": "friendship",
"showDatingPurpose": true,
"religion": "atheism",
"familyPlan": "no-maybe-in-future",
"financialState": "wealthy",
"bodyType": "fit",
"alcoholAttitude": "negative",
"smokingAttitude": "negative",
"educations": [{ "level": "bachelor", "inProgress": true }],
"languages": ["kk", "ru", "en"]
}'
A worked example is also kept in
api/e2e/profile.http.
Additional profile endpoints
| Method | Path | Description |
|---|---|---|
| GET | /profiles/photos | List photos with signed URLs |
| POST | /profiles/photos | Attach an uploaded media key (maximum 6 photos) |
| PATCH | /profiles/photos | Replace gallery order with all photo IDs |
| DELETE | /profiles/photos/:photoId | Remove a photo and its media object |
| GET | /profiles/batch?userIds=id1,id2 | Cacheable bulk lookup, max 100 |
| POST | /profiles/batch | Bulk lookup by JSON user IDs, max 100 |
| GET | /profiles/username/availability?username=... | Check a handle without reserving it |
| POST | /profiles/complete | Finish onboarding |
POST /profiles/complete requires the survey fields and at least one photo,
and returns missingFields; legal document versions may
be supplied but are currently optional. Profile photo URLs are signed for one
hour. avatarPhotoId selects the avatar independently of gallery order.
Public profile responses coarsen location and omit dating purpose or zodiac
when the owner's privacy toggle is off. The caller's own profile is
unredacted. GET bulk responses support private caching with ETag and 304.
Roadmap
POST /profiles/photos— Supabase Storage-backed avatars.- City-as-FK (Geonames) instead of free-text
hometown, with PostGIS-backed location for radius queries.