Concepts, value, and typical clone scenarios — less code.
Concepts, value, and typical clone scenarios — less code.
Preparing Ring content
Preparing Ring content
Preparing Ring content
REST for the dual-nature marketplace. The browse list is open to visitors; everything else needs a session. Browse list (GET /api/opportunities) is role-aware cursor pagination plus per-viewer enrichment. Search is a separate route. Like / Save / Hide / Contact are Server Actions, not HTTP resources.
Use Founder / Developer tabs to filter this page. Product loop and card UX: Opportunities.
Older API copy listed a fixed “7 types.” Runtime types are the OpportunityType union in features/opportunities/types/index.ts, gated by ring-config.json → opportunities.enabledTypes. Do not document a closed set of seven.
| Method | Path | Auth | Notes |
|---|---|---|---|
GET | /api/opportunities | Optional | limit, startAfter. Filter params types/type, q/query, categories/category, location, priority switch to the search service. Passes viewerUserId when signed in. Returns { opportunities, items, lastVisible, cursor, hasMore } |
POST | /api/opportunities | Session | Body createOpportunityBodySchema → { opportunity } 201 |
GET | /api/opportunities/{id} | Session | Single listing |
PUT / PATCH | /api/opportunities/{id} | Session | Update |
DELETE | /api/opportunities/{id} | Session | Delete |
GET | /api/opportunities/search | Session | Requires q. Optional types, categories, location, limit |
POST | /api/opportunities/search | Session | JSON body SearchOpportunitiesParams |
POST | /api/opportunities/{id}/sign | Session | Advisory petition signature, one per user. Returns signatureCount, signatureTarget |
GET | /api/opportunities/my | Session | Owner list; view= all | drafts | pending | active | archived |
POST | /api/opportunities/upload | Session | Attachments |
GET / POST | /api/mcp/v1/opportunities | MCP guard | List does not pass viewerUserId today |
Since 2026-10-17, unauthenticated GET /api/opportunities returns public, non-confidential rows for role visitor. The visitor filter runs after the page is fetched, so a guest page can hold fewer than limit rows. Detail, search and mutations still return 401 without a session.
/opportunities page loads GET /api/opportunities (a page of listings). Right-rail filters are sent as query params and applied on the server. Visitors can browse without an account.creator snapshot so the UI never prints a raw UUID.createOpportunity Server Action (app/_actions/opportunities.ts). REST POST is for API clients and MCP; both call the same service.public | subscriber | member | confidential | entity. entity rows are readable only by admins, the poster and members of the linked entity. List queries apply role-aware visibility filters (buildOpportunityVisibilityFilters)./apply REST route in alpha).Yes, public non-confidential rows only. GET /api/opportunities/{id} and search still require a session.
Yes when searchOpportunities has a session (or MCP actor) user id. The HTTP GET/POST search routes require a session.
createdBy gone?No. It remains the user id. creator is an optional public snapshot for UI.
MCP list omits viewerUserId, so attachOpportunityFeedFields is not applied on that path.
Prerequisite: product loop, feed card actions, and enabled-type gates.
Depends-on: cache tags, revalidatePath, and Tunnel opportunity:* after mutations.
Same-workflow: convert-to-request writes metadata.source consumed by list cards.
Same-workflow: organization linkage required on create (`organizationId` / linkedEntity).
REST for the dual-nature marketplace. The browse list is open to visitors; everything else needs a session. Browse list (GET /api/opportunities) is role-aware cursor pagination plus per-viewer enrichment. Search is a separate route. Like / Save / Hide / Contact are Server Actions, not HTTP resources.
Use Founder / Developer tabs to filter this page. Product loop and card UX: Opportunities.
Older API copy listed a fixed “7 types.” Runtime types are the OpportunityType union in features/opportunities/types/index.ts, gated by ring-config.json → opportunities.enabledTypes. Do not document a closed set of seven.
| Method | Path | Auth | Notes |
|---|---|---|---|
GET | /api/opportunities | Optional | limit, startAfter. Filter params types/type, q/query, categories/category, location, priority switch to the search service. Passes viewerUserId when signed in. Returns { opportunities, items, lastVisible, cursor, hasMore } |
POST | /api/opportunities | Session | Body createOpportunityBodySchema → { opportunity } 201 |
GET | /api/opportunities/{id} | Session | Single listing |
PUT / PATCH | /api/opportunities/{id} | Session | Update |
DELETE | /api/opportunities/{id} | Session | Delete |
GET | /api/opportunities/search | Session | Requires q. Optional types, categories, location, limit |
POST | /api/opportunities/search | Session | JSON body SearchOpportunitiesParams |
POST | /api/opportunities/{id}/sign | Session | Advisory petition signature, one per user. Returns signatureCount, signatureTarget |
GET | /api/opportunities/my | Session | Owner list; view= all | drafts | pending | active | archived |
POST | /api/opportunities/upload | Session | Attachments |
GET / POST | /api/mcp/v1/opportunities | MCP guard | List does not pass viewerUserId today |
Since 2026-10-17, unauthenticated GET /api/opportunities returns public, non-confidential rows for role visitor. The visitor filter runs after the page is fetched, so a guest page can hold fewer than limit rows. Detail, search and mutations still return 401 without a session.
/opportunities page loads GET /api/opportunities (a page of listings). Right-rail filters are sent as query params and applied on the server. Visitors can browse without an account.creator snapshot so the UI never prints a raw UUID.createOpportunity Server Action (app/_actions/opportunities.ts). REST POST is for API clients and MCP; both call the same service.public | subscriber | member | confidential | entity. entity rows are readable only by admins, the poster and members of the linked entity. List queries apply role-aware visibility filters (buildOpportunityVisibilityFilters)./apply REST route in alpha).Yes, public non-confidential rows only. GET /api/opportunities/{id} and search still require a session.
Yes when searchOpportunities has a session (or MCP actor) user id. The HTTP GET/POST search routes require a session.
createdBy gone?No. It remains the user id. creator is an optional public snapshot for UI.
MCP list omits viewerUserId, so attachOpportunityFeedFields is not applied on that path.
Prerequisite: product loop, feed card actions, and enabled-type gates.
Depends-on: cache tags, revalidatePath, and Tunnel opportunity:* after mutations.
Same-workflow: convert-to-request writes metadata.source consumed by list cards.
Same-workflow: organization linkage required on create (`organizationId` / linkedEntity).
REST for the dual-nature marketplace. The browse list is open to visitors; everything else needs a session. Browse list (GET /api/opportunities) is role-aware cursor pagination plus per-viewer enrichment. Search is a separate route. Like / Save / Hide / Contact are Server Actions, not HTTP resources.
Use Founder / Developer tabs to filter this page. Product loop and card UX: Opportunities.
Older API copy listed a fixed “7 types.” Runtime types are the OpportunityType union in features/opportunities/types/index.ts, gated by ring-config.json → opportunities.enabledTypes. Do not document a closed set of seven.
| Method | Path | Auth | Notes |
|---|---|---|---|
GET | /api/opportunities | Optional | limit, startAfter. Filter params types/type, q/query, categories/category, location, priority switch to the search service. Passes viewerUserId when signed in. Returns { opportunities, items, lastVisible, cursor, hasMore } |
POST | /api/opportunities | Session | Body createOpportunityBodySchema → { opportunity } 201 |
GET | /api/opportunities/{id} | Session | Single listing |
PUT / PATCH | /api/opportunities/{id} | Session | Update |
DELETE | /api/opportunities/{id} | Session | Delete |
GET | /api/opportunities/search | Session | Requires q. Optional types, categories, location, limit |
POST | /api/opportunities/search | Session | JSON body SearchOpportunitiesParams |
POST | /api/opportunities/{id}/sign | Session | Advisory petition signature, one per user. Returns signatureCount, signatureTarget |
GET | /api/opportunities/my | Session | Owner list; view= all | drafts | pending | active | archived |
POST | /api/opportunities/upload | Session | Attachments |
GET / POST | /api/mcp/v1/opportunities | MCP guard | List does not pass viewerUserId today |
Since 2026-10-17, unauthenticated GET /api/opportunities returns public, non-confidential rows for role visitor. The visitor filter runs after the page is fetched, so a guest page can hold fewer than limit rows. Detail, search and mutations still return 401 without a session.
/opportunities page loads GET /api/opportunities (a page of listings). Right-rail filters are sent as query params and applied on the server. Visitors can browse without an account.creator snapshot so the UI never prints a raw UUID.createOpportunity Server Action (app/_actions/opportunities.ts). REST POST is for API clients and MCP; both call the same service.public | subscriber | member | confidential | entity. entity rows are readable only by admins, the poster and members of the linked entity. List queries apply role-aware visibility filters (buildOpportunityVisibilityFilters)./apply REST route in alpha).Yes, public non-confidential rows only. GET /api/opportunities/{id} and search still require a session.
Yes when searchOpportunities has a session (or MCP actor) user id. The HTTP GET/POST search routes require a session.
createdBy gone?No. It remains the user id. creator is an optional public snapshot for UI.
MCP list omits viewerUserId, so attachOpportunityFeedFields is not applied on that path.
Prerequisite: product loop, feed card actions, and enabled-type gates.
Depends-on: cache tags, revalidatePath, and Tunnel opportunity:* after mutations.
Same-workflow: convert-to-request writes metadata.source consumed by list cards.
Same-workflow: organization linkage required on create (`organizationId` / linkedEntity).
curl against session cookies on a local clone.
| Action | Module |
|---|---|
| Like | toggleOpportunityLike → like-service / likes |
| Save | toggleOpportunitySave → user_content_interactions |
| Hide | markOpportunityNotInterested → user_content_interactions |
| Comment | createOpportunityComment |
| Contact intent | recordOpportunityContactIntent (Matcher; UI still router.push /messages?user=) |
File: app/_actions/opportunity-interactions.ts.
not-inmapDbQueryRowToSerializedOpportunity() in features/opportunities/lib/opportunity-db-mapper.ts. Do not use lib/converters/opportunity-converter.ts on Postgres.
Operator 'not-in' is on IDatabaseService DatabaseFilterOperator. PostgreSQL: NOT (field = ANY($n)). Firebase adapter post-filters.
Creates, updates, deletes, and status changes call syncOpportunityDiscovery() which:
revalidateTag — invalidates unstable_cache opportunity lists (lib/cached-data.ts)revalidatePath — /[locale]/opportunities, /[locale]/opportunities/[id], /[locale]/opportunities/myopportunity:created|updated|deleted on channel opportunitiesThere is no separate search-index reindex command. See Discovery mutation sync. Client browse still uses useCursorFeed (24h); revalidatePath alone does not clear that cache.
UI create: useActionState + createOpportunity from app/_actions/opportunities.ts — API integration.
Next-step: curl against a local session cookie.
curl against session cookies on a local clone.
| Action | Module |
|---|---|
| Like | toggleOpportunityLike → like-service / likes |
| Save | toggleOpportunitySave → user_content_interactions |
| Hide | markOpportunityNotInterested → user_content_interactions |
| Comment | createOpportunityComment |
| Contact intent | recordOpportunityContactIntent (Matcher; UI still router.push /messages?user=) |
File: app/_actions/opportunity-interactions.ts.
not-inmapDbQueryRowToSerializedOpportunity() in features/opportunities/lib/opportunity-db-mapper.ts. Do not use lib/converters/opportunity-converter.ts on Postgres.
Operator 'not-in' is on IDatabaseService DatabaseFilterOperator. PostgreSQL: NOT (field = ANY($n)). Firebase adapter post-filters.
Creates, updates, deletes, and status changes call syncOpportunityDiscovery() which:
revalidateTag — invalidates unstable_cache opportunity lists (lib/cached-data.ts)revalidatePath — /[locale]/opportunities, /[locale]/opportunities/[id], /[locale]/opportunities/myopportunity:created|updated|deleted on channel opportunitiesThere is no separate search-index reindex command. See Discovery mutation sync. Client browse still uses useCursorFeed (24h); revalidatePath alone does not clear that cache.
UI create: useActionState + createOpportunity from app/_actions/opportunities.ts — API integration.
Next-step: curl against a local session cookie.
curl against session cookies on a local clone.
| Action | Module |
|---|---|
| Like | toggleOpportunityLike → like-service / likes |
| Save | toggleOpportunitySave → user_content_interactions |
| Hide | markOpportunityNotInterested → user_content_interactions |
| Comment | createOpportunityComment |
| Contact intent | recordOpportunityContactIntent (Matcher; UI still router.push /messages?user=) |
File: app/_actions/opportunity-interactions.ts.
not-inmapDbQueryRowToSerializedOpportunity() in features/opportunities/lib/opportunity-db-mapper.ts. Do not use lib/converters/opportunity-converter.ts on Postgres.
Operator 'not-in' is on IDatabaseService DatabaseFilterOperator. PostgreSQL: NOT (field = ANY($n)). Firebase adapter post-filters.
Creates, updates, deletes, and status changes call syncOpportunityDiscovery() which:
revalidateTag — invalidates unstable_cache opportunity lists (lib/cached-data.ts)revalidatePath — /[locale]/opportunities, /[locale]/opportunities/[id], /[locale]/opportunities/myopportunity:created|updated|deleted on channel opportunitiesThere is no separate search-index reindex command. See Discovery mutation sync. Client browse still uses useCursorFeed (24h); revalidatePath alone does not clear that cache.
UI create: useActionState + createOpportunity from app/_actions/opportunities.ts — API integration.
Next-step: curl against a local session cookie.