Skip to content

feat: add public freelancer profile API - #169

Merged
SudiptaPaul-31 merged 1 commit into
Lumina-eX:mainfrom
3m1n3nc3:feat/public-freelancer-profile-api
Jul 27, 2026
Merged

feat: add public freelancer profile API#169
SudiptaPaul-31 merged 1 commit into
Lumina-eX:mainfrom
3m1n3nc3:feat/public-freelancer-profile-api

Conversation

@3m1n3nc3

Copy link
Copy Markdown
Contributor

What

Adds the public freelancer profile API: four read-only, unauthenticated endpoints under a new /api/public/freelancers/{id} namespace.

Endpoint Returns
GET /api/public/freelancers/{id} Profile — username, bio, skills, availability, rating summary, work-history counts
GET /api/public/freelancers/{id}/contracts Paginated completed contracts, newest first
GET /api/public/freelancers/{id}/reviews Paginated client reviews + ?verified= filter
GET /api/public/freelancers/{id}/reputation Aggregated 0–100 score with per-component breakdown

Query logic, types, sanitisation, parsing and response helpers live in lib/publicFreelancerProfile.ts, following the existing lib/freelancerDiscovery.ts pattern. Full integration guide in docs/public-freelancer-profile-api.md, linked from the README.

Why

Closes #154. External consumers and clients need a stable, documented surface for freelancer data that exposes public information only, rather than reaching into endpoints built for authenticated dashboard use.

Acceptance criteria

Authorization enforced. Handlers copy fields explicitly instead of spreading rows, so users.email, contracts.terms, client identity, escrow wiring and reviews.reviewer_id cannot leak even if columns are added to those tables later. A test injects every sensitive column into the mocked rows and asserts none reaches the response. Client-only accounts return the same 404 as unknown ids, so these endpoints cannot be used to enumerate users. Rate limited to 120 req/min per IP per endpoint.

Efficient queries. One database round-trip per endpoint — two for sub-resources, which confirm the freelancer exists first so an unknown id returns 404 rather than an empty page. Profile aggregates ride along as scalar sub-selects; list totals and the review summary come from COUNT(*) OVER() / AVG(...) OVER() in the same statement (window functions run before LIMIT, so they cover the whole filtered set, not the page). Reputation reads the pre-aggregated freelancer_reputation snapshot rather than recomputing. limit capped at 100. Partial and composite indexes added in scripts/011-public-profile-indexes.sql; the completed-contract and active-job indexes are partial so they stay small as history grows. Responses are edge-cacheable for 60s with a 300s stale-while-revalidate window.

Consistent schema. Every response is { data, meta }; every error is { error, code } — branch on code, not on error. Money is returned as decimal strings (total_amount is DECIMAL(18,6), so JSON numbers would lose precision), timestamps are ISO-8601 UTC, and "no data" is null while aggregate counts are 0.

API documentation provided. docs/public-freelancer-profile-api.md covers the envelope, every field, all error codes, curl examples with real payloads, the exact reputation formula, the withheld-field list with rationale, and the performance notes above.

Technical considerations worth reviewing

Two calls I'd like a second opinion on:

1. availability is derived, not stored. The schema has no availability column, so rather than adding one nothing can set, the profile reports busy when the freelancer has a job in assigned / in_progress / in_review, otherwise available. Documented as derived; if explicit freelancer-set availability lands later, the field keeps its shape and only changes source.

2. The reputation score is new, not the existing one. The issue asks for a score based on completed contracts, reviews and platform metrics, but the existing reputationScore ignores reviews entirely. This endpoint returns a composite blend — completion .30, on-time .25, dispute-free .20, client rating .25 — where components with no data are dropped and their weight is redistributed proportionally, so a freelancer with no reviews yet is scored on delivery history rather than penalised for the gap. components[] is returned so consumers can show the breakdown instead of treating the number as a black box. GET /api/freelancers/{userId}/reputation is untouched; the docs explain how the two relate.

On walletAddress: deliberately included. It is the freelancer's on-chain identity, already public on the ledger, is what makes escrow payments independently verifiable, and is already returned by the existing GET /api/freelancers listing. Happy to drop it if maintainers would rather keep the public surface narrower.

Database

psql "$DATABASE_URL" -f scripts/011-public-profile-indexes.sql

Expose read-only, unauthenticated endpoints under /api/public/freelancers/[id]
so external consumers and clients can pull a freelancer's public record:

  GET /api/public/freelancers/[id]              profile
  GET /api/public/freelancers/[id]/contracts    completed contracts
  GET /api/public/freelancers/[id]/reviews      client reviews
  GET /api/public/freelancers/[id]/reputation   aggregated reputation score

Query logic, types, sanitisation and parsing live in
lib/publicFreelancerProfile.ts, following the existing freelancerDiscovery
pattern.

Public data only: handlers copy fields explicitly instead of spreading rows, so
users.email, contracts.terms, client identity, escrow wiring and
reviews.reviewer_id cannot leak even if columns are added later. Client-only
accounts return the same 404 as unknown ids, so the endpoints cannot be used to
enumerate users.

Consistent schema: every response is { data, meta } and every error is
{ error, code }; money as decimal strings, timestamps ISO-UTC, null for
"no data".

Efficient queries: one round-trip per endpoint (two for sub-resources, which
confirm existence first so an unknown id 404s instead of returning an empty
page); totals and the review summary come from COUNT(*) OVER() / AVG(...) OVER()
in the same statement; reputation reads the pre-aggregated
freelancer_reputation snapshot; limit capped at 100; partial and composite
indexes added in scripts/011-public-profile-indexes.sql.

availability is derived from in-flight work rather than stored, since the schema
has no availability column.

The reputation score is a new composite blend of completed contracts, client
reviews and delivery metrics, with per-component weights redistributed when a
signal is missing. The existing /api/freelancers/[userId]/reputation endpoint is
unchanged.

Adds docs/public-freelancer-profile-api.md and 59 tests.
@drips-wave

drips-wave Bot commented Jul 26, 2026

Copy link
Copy Markdown

@3m1n3nc3 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@SudiptaPaul-31
SudiptaPaul-31 merged commit 57ccf58 into Lumina-eX:main Jul 27, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Public Freelancer Profile API

2 participants