Skip to main content

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​

EndpointStatus
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​

StatusCause
401Missing / invalid bearer token
404Profile 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​

StatusCause
401Missing / invalid bearer token
404Profile 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​

FieldTypeRequiredDescription
firstNamestring✅Display name
Other survey fieldsmixedOptional during creation; required to complete onboarding
showDatingPurpose, showZodiacbooleanPrivacy toggles, default false
educations, languagesarraysDefault to empty arrays
city, sex, latitude, longitude, isVisibleOnMapmixedCurrent 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:

FieldSource
userIdSupabase user from the JWT
zodiacDerived 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" }
]
}
StatusCause
400Validation failed (see body)
401Missing / invalid bearer token
500Internal 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​

StatusCause
400Validation failed
401Missing / invalid bearer token
404Profile not found

Delete profile​

Permanently deletes the authenticated user's profile.

Request​

DELETE /profiles
Authorization: Bearer <supabase-access-token>

Response​

{ "deleted": true }

Errors​

StatusCause
401Missing / invalid bearer token
404Profile 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​

MethodPathDescription
GET/profiles/photosList photos with signed URLs
POST/profiles/photosAttach an uploaded media key (maximum 6 photos)
PATCH/profiles/photosReplace gallery order with all photo IDs
DELETE/profiles/photos/:photoIdRemove a photo and its media object
GET/profiles/batch?userIds=id1,id2Cacheable bulk lookup, max 100
POST/profiles/batchBulk lookup by JSON user IDs, max 100
GET/profiles/username/availability?username=...Check a handle without reserving it
POST/profiles/completeFinish 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.