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
Ring Opportunities is the supply ↔ demand loop for each clone: members post offers and requests (plus specialized types), Matcher scores interested users, and notifications invite them to connect — without waiting for someone to search the feed.
The browse list at /opportunities is a named feed: each card shows a public-safe poster, human tags, and Like / Save / Hide / Contact. Live inserts still arrive over Tunnel; the browse pane no longer shows a websocket status bar.
Use Founder / Developer tabs in the docs sidebar to filter this page. audience frontmatter controls in-page blocks; sidebar visibility is controlled by lib/docs/audience-curated-docs.ts.
Older copy claimed fixed accuracy %, hire-cost savings, and “15k+ users.” Those figures are deprecated. Live numbers come from Admin → Matcher analytics and clone traffic. Matcher thresholds are config (ring-config.json → matcher, overridable in platform settings) — not kingdom-wide SLAs.
| Previous | Ring equivalent |
|---|---|
| Post job → hope someone searches | Post opportunity → Matcher scores candidates → notify with short explanations |
| Offers only (employer → talent) | Dual-nature: offer and request plus specialized types |
| One listing taxonomy | White-label opportunities.enabledTypes gate (16 defaults in Layer1 / org config) |
| Manual enrichment | Optional LLM auto-fill on create (OpportunityAutoFillService) |
| Opaque ranking | Eight scored factors (MatchFactors in lib/ai/types.ts) + configurable scoreThreshold |
| Creator UUID on cards | Public-safe name / avatar; username only when publicProfile is true |
sourceMessageId: chips | Hidden machine tags; owner-only From chat when provenance exists |
| “Live Updates Active / via websocket” on browse | Silent useRealtimeOpportunities; the status bar remains on detail only |
| Separate funding jars | Collective / builder jars via Public Pools (donation path live; on-chain escrow gated) |
Default Matcher install values (ring-config.json → matcher): scoreThreshold 0.7, maxMatches 10, autoApprove false, autoApproveMinScore 0.7, llmConfidenceGate 0.8. Env can override auto-approve via MATCHER_AUTO_APPROVE / MATCHER_AUTO_APPROVE_MIN_SCORE (see features/admin/platform-settings/matcher-config.ts). Cap matches with MAX_MATCHES_PER_OPPORTUNITY.
Scoring factors, LLM vs heuristic paths, and explanation length live on AI Matcher. Thresholds above are the install defaults — not a published accuracy KPI.
Opportunities turn your Ring into a local marketplace of intent: people publish what they can provide and what they need. Matcher pushes relevant invitations instead of relying on feed scroll. Specialized types (jobs, collective orders, scheduled services, program/investment → CRM, Ring customization quests) are gated per clone so you ship only the loops your vertical needs.
Use the ring-ai-matcher-specialist (or Cursor skill of the same family) with a pause-for-approval prompt:
“Audit this clone’s Opportunities loop: list
opportunities.enabledTypesandmatcherfromring-config.json, confirm LLM env is set, dry-run create of oneofferand onerequest, report MatchFactors and whetherautoApprovewould promote pending→active. Open/opportunitiesas two users and confirm cards show names (not UUIDs), Hide excludes on reload, and Contact lands on/messages?user=. Do not change platform_settings until I approve.”
Inputs checklist: clone key, admin access, LLM credentials present, sample entity id for organizationId.
Set opportunities.enabledTypes and matcher.* in ring-config.json; redeploy or reload config snapshot as your clone does for ring-config.
In Admin → Matcher / AI settings, leave auto-approve off; set scoreThreshold / maxMatches to match your vertical tolerance.
Create a test opportunity from the type selector UI (or POST /api/opportunities); open My Opportunities at /opportunities/my.
Confirm a matched user received a notification and that discovery lists refresh (Tunnel / soft refresh) without inventing a search-index reindex step. Confirm the browse list has no websocket status bar.
No. Default matcher.autoApprove is false. When enabled, maybeAutoApproveOpportunity still requires score and LLM confidence gates (autoApproveMinScore, llmConfidenceGate).
No. Treat historical marketing percentages as deprecated. Use Admin Matcher analytics for your tenant.
No. Authenticated viewers see name/avatar. Username is included only when publicProfile is true. There is no postAnonymously flag.
Only when DB_BACKEND_MODE=firebase-full. PostgreSQL clones use opportunity-db-mapper.ts and SerializedOpportunity.
/opportunities/my-opportunities go?Removed. Canonical dashboard is /opportunities/my via ROUTES.MY_OPPORTUNITIES.
sourceMessageId: chips disappear?They were machine provenance, not user tags. New conversions write metadata.source; the card hides legacy encoded tags and shows From chat to the owner when a conversation id exists.
Check LLM availability, Matcher score threshold, maxMatches, and entity block filters in matcher-notification-filter. Notifications go through features/notifications/services/notification-service.
The cursor-feed cache is keyed per viewer. Hide is stored on user_content_interactions for that user; the next getOpportunitiesForRole with that viewerUserId applies not-in.
Opportunity specializations can carry JSONB metadata (e.g. collective order slots). Community funding jars and builder payout SSOT are documented under Public Pools — do not invent PSP “donation product” APIs as the jar path.
Next-step: REST route contract, list enrichment fields, and search vs browse pagination.
Depends-on: how syncOpportunityDiscovery invalidates cache and publishes Tunnel events.
Same-workflow: convertTaskToOpportunity writes metadata.source for the From chat chip.
Same-workflow: Contact uses /messages?user=; From chat uses /messages?c=.
Ring Opportunities is the supply ↔ demand loop for each clone: members post offers and requests (plus specialized types), Matcher scores interested users, and notifications invite them to connect — without waiting for someone to search the feed.
The browse list at /opportunities is a named feed: each card shows a public-safe poster, human tags, and Like / Save / Hide / Contact. Live inserts still arrive over Tunnel; the browse pane no longer shows a websocket status bar.
Use Founder / Developer tabs in the docs sidebar to filter this page. audience frontmatter controls in-page blocks; sidebar visibility is controlled by lib/docs/audience-curated-docs.ts.
Older copy claimed fixed accuracy %, hire-cost savings, and “15k+ users.” Those figures are deprecated. Live numbers come from Admin → Matcher analytics and clone traffic. Matcher thresholds are config (ring-config.json → matcher, overridable in platform settings) — not kingdom-wide SLAs.
| Previous | Ring equivalent |
|---|---|
| Post job → hope someone searches | Post opportunity → Matcher scores candidates → notify with short explanations |
| Offers only (employer → talent) | Dual-nature: offer and request plus specialized types |
| One listing taxonomy | White-label opportunities.enabledTypes gate (16 defaults in Layer1 / org config) |
| Manual enrichment | Optional LLM auto-fill on create (OpportunityAutoFillService) |
| Opaque ranking | Eight scored factors (MatchFactors in lib/ai/types.ts) + configurable scoreThreshold |
| Creator UUID on cards | Public-safe name / avatar; username only when publicProfile is true |
sourceMessageId: chips | Hidden machine tags; owner-only From chat when provenance exists |
| “Live Updates Active / via websocket” on browse | Silent useRealtimeOpportunities; the status bar remains on detail only |
| Separate funding jars | Collective / builder jars via Public Pools (donation path live; on-chain escrow gated) |
Default Matcher install values (ring-config.json → matcher): scoreThreshold 0.7, maxMatches 10, autoApprove false, autoApproveMinScore 0.7, llmConfidenceGate 0.8. Env can override auto-approve via MATCHER_AUTO_APPROVE / MATCHER_AUTO_APPROVE_MIN_SCORE (see features/admin/platform-settings/matcher-config.ts). Cap matches with MAX_MATCHES_PER_OPPORTUNITY.
Scoring factors, LLM vs heuristic paths, and explanation length live on AI Matcher. Thresholds above are the install defaults — not a published accuracy KPI.
Opportunities turn your Ring into a local marketplace of intent: people publish what they can provide and what they need. Matcher pushes relevant invitations instead of relying on feed scroll. Specialized types (jobs, collective orders, scheduled services, program/investment → CRM, Ring customization quests) are gated per clone so you ship only the loops your vertical needs.
Use the ring-ai-matcher-specialist (or Cursor skill of the same family) with a pause-for-approval prompt:
“Audit this clone’s Opportunities loop: list
opportunities.enabledTypesandmatcherfromring-config.json, confirm LLM env is set, dry-run create of oneofferand onerequest, report MatchFactors and whetherautoApprovewould promote pending→active. Open/opportunitiesas two users and confirm cards show names (not UUIDs), Hide excludes on reload, and Contact lands on/messages?user=. Do not change platform_settings until I approve.”
Inputs checklist: clone key, admin access, LLM credentials present, sample entity id for organizationId.
Set opportunities.enabledTypes and matcher.* in ring-config.json; redeploy or reload config snapshot as your clone does for ring-config.
In Admin → Matcher / AI settings, leave auto-approve off; set scoreThreshold / maxMatches to match your vertical tolerance.
Create a test opportunity from the type selector UI (or POST /api/opportunities); open My Opportunities at /opportunities/my.
Confirm a matched user received a notification and that discovery lists refresh (Tunnel / soft refresh) without inventing a search-index reindex step. Confirm the browse list has no websocket status bar.
No. Default matcher.autoApprove is false. When enabled, maybeAutoApproveOpportunity still requires score and LLM confidence gates (autoApproveMinScore, llmConfidenceGate).
No. Treat historical marketing percentages as deprecated. Use Admin Matcher analytics for your tenant.
No. Authenticated viewers see name/avatar. Username is included only when publicProfile is true. There is no postAnonymously flag.
Only when DB_BACKEND_MODE=firebase-full. PostgreSQL clones use opportunity-db-mapper.ts and SerializedOpportunity.
/opportunities/my-opportunities go?Removed. Canonical dashboard is /opportunities/my via ROUTES.MY_OPPORTUNITIES.
sourceMessageId: chips disappear?They were machine provenance, not user tags. New conversions write metadata.source; the card hides legacy encoded tags and shows From chat to the owner when a conversation id exists.
Check LLM availability, Matcher score threshold, maxMatches, and entity block filters in matcher-notification-filter. Notifications go through features/notifications/services/notification-service.
The cursor-feed cache is keyed per viewer. Hide is stored on user_content_interactions for that user; the next getOpportunitiesForRole with that viewerUserId applies not-in.
Opportunity specializations can carry JSONB metadata (e.g. collective order slots). Community funding jars and builder payout SSOT are documented under Public Pools — do not invent PSP “donation product” APIs as the jar path.
Next-step: REST route contract, list enrichment fields, and search vs browse pagination.
Depends-on: how syncOpportunityDiscovery invalidates cache and publishes Tunnel events.
Same-workflow: convertTaskToOpportunity writes metadata.source for the From chat chip.
Same-workflow: Contact uses /messages?user=; From chat uses /messages?c=.
Ring Opportunities is the supply ↔ demand loop for each clone: members post offers and requests (plus specialized types), Matcher scores interested users, and notifications invite them to connect — without waiting for someone to search the feed.
The browse list at /opportunities is a named feed: each card shows a public-safe poster, human tags, and Like / Save / Hide / Contact. Live inserts still arrive over Tunnel; the browse pane no longer shows a websocket status bar.
Use Founder / Developer tabs in the docs sidebar to filter this page. audience frontmatter controls in-page blocks; sidebar visibility is controlled by lib/docs/audience-curated-docs.ts.
Older copy claimed fixed accuracy %, hire-cost savings, and “15k+ users.” Those figures are deprecated. Live numbers come from Admin → Matcher analytics and clone traffic. Matcher thresholds are config (ring-config.json → matcher, overridable in platform settings) — not kingdom-wide SLAs.
| Previous | Ring equivalent |
|---|---|
| Post job → hope someone searches | Post opportunity → Matcher scores candidates → notify with short explanations |
| Offers only (employer → talent) | Dual-nature: offer and request plus specialized types |
| One listing taxonomy | White-label opportunities.enabledTypes gate (16 defaults in Layer1 / org config) |
| Manual enrichment | Optional LLM auto-fill on create (OpportunityAutoFillService) |
| Opaque ranking | Eight scored factors (MatchFactors in lib/ai/types.ts) + configurable scoreThreshold |
| Creator UUID on cards | Public-safe name / avatar; username only when publicProfile is true |
sourceMessageId: chips | Hidden machine tags; owner-only From chat when provenance exists |
| “Live Updates Active / via websocket” on browse | Silent useRealtimeOpportunities; the status bar remains on detail only |
| Separate funding jars | Collective / builder jars via Public Pools (donation path live; on-chain escrow gated) |
Default Matcher install values (ring-config.json → matcher): scoreThreshold 0.7, maxMatches 10, autoApprove false, autoApproveMinScore 0.7, llmConfidenceGate 0.8. Env can override auto-approve via MATCHER_AUTO_APPROVE / MATCHER_AUTO_APPROVE_MIN_SCORE (see features/admin/platform-settings/matcher-config.ts). Cap matches with MAX_MATCHES_PER_OPPORTUNITY.
Scoring factors, LLM vs heuristic paths, and explanation length live on AI Matcher. Thresholds above are the install defaults — not a published accuracy KPI.
Opportunities turn your Ring into a local marketplace of intent: people publish what they can provide and what they need. Matcher pushes relevant invitations instead of relying on feed scroll. Specialized types (jobs, collective orders, scheduled services, program/investment → CRM, Ring customization quests) are gated per clone so you ship only the loops your vertical needs.
Use the ring-ai-matcher-specialist (or Cursor skill of the same family) with a pause-for-approval prompt:
“Audit this clone’s Opportunities loop: list
opportunities.enabledTypesandmatcherfromring-config.json, confirm LLM env is set, dry-run create of oneofferand onerequest, report MatchFactors and whetherautoApprovewould promote pending→active. Open/opportunitiesas two users and confirm cards show names (not UUIDs), Hide excludes on reload, and Contact lands on/messages?user=. Do not change platform_settings until I approve.”
Inputs checklist: clone key, admin access, LLM credentials present, sample entity id for organizationId.
Set opportunities.enabledTypes and matcher.* in ring-config.json; redeploy or reload config snapshot as your clone does for ring-config.
In Admin → Matcher / AI settings, leave auto-approve off; set scoreThreshold / maxMatches to match your vertical tolerance.
Create a test opportunity from the type selector UI (or POST /api/opportunities); open My Opportunities at /opportunities/my.
Confirm a matched user received a notification and that discovery lists refresh (Tunnel / soft refresh) without inventing a search-index reindex step. Confirm the browse list has no websocket status bar.
No. Default matcher.autoApprove is false. When enabled, maybeAutoApproveOpportunity still requires score and LLM confidence gates (autoApproveMinScore, llmConfidenceGate).
No. Treat historical marketing percentages as deprecated. Use Admin Matcher analytics for your tenant.
No. Authenticated viewers see name/avatar. Username is included only when publicProfile is true. There is no postAnonymously flag.
Only when DB_BACKEND_MODE=firebase-full. PostgreSQL clones use opportunity-db-mapper.ts and SerializedOpportunity.
/opportunities/my-opportunities go?Removed. Canonical dashboard is /opportunities/my via ROUTES.MY_OPPORTUNITIES.
sourceMessageId: chips disappear?They were machine provenance, not user tags. New conversions write metadata.source; the card hides legacy encoded tags and shows From chat to the owner when a conversation id exists.
Check LLM availability, Matcher score threshold, maxMatches, and entity block filters in matcher-notification-filter. Notifications go through features/notifications/services/notification-service.
The cursor-feed cache is keyed per viewer. Hide is stored on user_content_interactions for that user; the next getOpportunitiesForRole with that viewerUserId applies not-in.
Opportunity specializations can carry JSONB metadata (e.g. collective order slots). Community funding jars and builder payout SSOT are documented under Public Pools — do not invent PSP “donation product” APIs as the jar path.
Next-step: REST route contract, list enrichment fields, and search vs browse pagination.
Depends-on: how syncOpportunityDiscovery invalidates cache and publishes Tunnel events.
Same-workflow: convertTaskToOpportunity writes metadata.source for the From chat chip.
Same-workflow: Contact uses /messages?user=; From chat uses /messages?c=.
/opportunities)Authenticated members see compact cards: type chip, title, two-line description, poster byline, relative posted time, human tags, like count, and icon actions.
| Action | Who | What happens |
|---|---|---|
| Like | Signed-in viewer | Toggles the likes collection; heart fills from opportunity.viewer.liked |
| Save | Signed-in viewer | Toggles user_content_interactions (save); hydrates from viewer.saved |
| Hide | Signed-in viewer | Marks not_interested; in-session Hidden · Undo; next list query excludes the id |
| Contact | Subscriber+; hidden on own posts | Navigates immediately to /messages?user=<createdBy>; Matcher contact_intent is best-effort and must not gate navigation |
| From chat | Owner only, when the listing came from a task | Opens /messages?c=<conversationId> |
There is no anonymous-posting flag. Authenticated viewers see the poster name/avatar. Username appears only on public profiles. Empty name renders Member vs Private User.
Right-rail query params (q, types, location, …) filter the current page of cards in the browser. GET /api/opportunities still paginates with limit / startAfter only — it is not a full-text search endpoint. Use Opportunities API search for q=.
My Opportunities at /opportunities/my is the owner dashboard (Edit / Delete / View). Saved and Applied tab counts are hardcoded 0 today — do not treat them as live metrics.
job, offer, request, partnership; keep Matcher auto-approve off until you trust scoring.collective_order, scheduled_services, asset_rental, bounty, tender with Wallet / PaymentConductor rails.ring_customization and program so customization quests and institution programs land in CRM.opportunities.opportunities.enabledTypes in ring-config.json (do not assume every OpportunityType union member is UI-visible).LLM_PROVIDER, OPENAI_API_KEY or ANTHROPIC_API_KEY, LLM_MODEL) before expecting auto-fill / LLM explanations./opportunities/my (ROUTES.MY_OPPORTUNITIES) — legacy /opportunities/my-opportunities is removed.| Viewer like / save / hide |
get-viewer-interactions.ts ← likes + user_content_interactions |
| Tag / chat provenance | lib/opportunity-tags.ts (splitOpportunityTags) |
| Feed card / list | opportunity-feed-card.tsx, opportunity-list.tsx |
| Interactions | app/_actions/opportunity-interactions.ts |
| Matcher core | lib/ai/matcher.ts + OpportunityMatchingService |
| Auto-fill | features/opportunities/services/auto-fill-service.ts |
| Auto-approve | features/opportunities/services/auto-approval-service.ts |
| PG JSONB ↔ client | features/opportunities/lib/opportunity-db-mapper.ts |
| Post-mutation sync | features/opportunities/lib/opportunity-mutation-sync.ts → syncOpportunityDiscovery |
| Program → CRM | features/opportunities/lib/program-crm-ingest.ts |
| UI Server Actions | app/_actions/opportunities.ts |
| Firestore legacy | lib/converters/opportunity-converter.ts when DB_BACKEND_MODE=firebase-full only |
Database: default production clones use DB_BACKEND_MODE=k8s-postgres-fcm (also supabase-fcm). Services return SerializedOpportunity (ISO date strings). Do not invent DATABASE_MODE=firebase_only.
(protected)/opportunities/page.tsx and GET /api/opportunities pass viewerUserId: session.user.id into getOpportunitiesForRole. Hidden ids are loaded first, then the query uses DatabaseService operator not-in on opportunities.id. PostgreSQLAdapter maps opportunities / likes id to the SQL column (not data->>'id').
attachOpportunityFeedFields adds creator, viewer, and likes. Cards hydrate useOptimistic from opportunity.viewer (the previous constant-base optimistic state never showed saved/liked). After Like / Save / Hide, onInteractionChange patches the cursor-feed item so session flags stay consistent.
useCursorFeed persists 24h in localStorage. The list fingerprint is …|viewer=<userId> so one account cannot inherit another’s like/save/hide. Reloads overlay server initialItems onto cached rows by id (hooks/use-cursor-feed.ts).
Browse useRealtimeOpportunities({ autoConnect: true }) stays for silent tunnel subscribe. Create snippets include creator { id, name, avatar } so live-inserted cards are not “Private User”. Detail (opportunity-details.tsx) still renders the Live Updates bar.
convertTaskToOpportunity writes metadata.source: { kind: 'chat_task', messageId, conversationId } and tags: []. splitOpportunityTags still parses legacy sourceMessageId: / sourceConversationId: / chat_task tags at render. There is no backfill job.
public | subscriber | member | confidentialdraft | pending | active | closed | expired | archivedcanCreateOpportunityConfidential / canEditOpportunity)| Bucket | Types (examples) | Minimum role |
|---|---|---|
| Member offers | offer, job, partnership, volunteer, mentorship, resource, event, ring_customization, program, collective_order, tender, asset_rental | member privileges |
| Requests | request | subscriber+ |
| Subscriber specials | cv, scheduled_services, bounty | subscriber+ |
| Other union members | e.g. future-facing types not in enabled/permission sets | admin+ (and must be enabled) |
Route contracts, create body, and list JSON live on Opportunities API. Like / Save / Hide / Contact are Server Actions (app/_actions/opportunity-interactions.ts), not REST. MCP GET /api/mcp/v1/opportunities currently does not pass viewerUserId.
After mutations, syncOpportunityDiscovery invalidates cache tags, revalidatePath for list/detail/my, and publishes Tunnel opportunity:*. That does not bust client useCursorFeed — overlay + viewer fingerprint are the browse freshness path.
Keys documented in env.local.template: LLM_PROVIDER, OPENAI_API_KEY, ANTHROPIC_API_KEY, LLM_MODEL, MAX_MATCHES_PER_OPPORTUNITY, MATCHING_MAX_TOKENS. Without a working provider, Matcher falls back to non-LLM / tag-style explanations with lower confidence.
Same-workflow: organization linkage and verified posters for opportunities.
/opportunities)Authenticated members see compact cards: type chip, title, two-line description, poster byline, relative posted time, human tags, like count, and icon actions.
| Action | Who | What happens |
|---|---|---|
| Like | Signed-in viewer | Toggles the likes collection; heart fills from opportunity.viewer.liked |
| Save | Signed-in viewer | Toggles user_content_interactions (save); hydrates from viewer.saved |
| Hide | Signed-in viewer | Marks not_interested; in-session Hidden · Undo; next list query excludes the id |
| Contact | Subscriber+; hidden on own posts | Navigates immediately to /messages?user=<createdBy>; Matcher contact_intent is best-effort and must not gate navigation |
| From chat | Owner only, when the listing came from a task | Opens /messages?c=<conversationId> |
There is no anonymous-posting flag. Authenticated viewers see the poster name/avatar. Username appears only on public profiles. Empty name renders Member vs Private User.
Right-rail query params (q, types, location, …) filter the current page of cards in the browser. GET /api/opportunities still paginates with limit / startAfter only — it is not a full-text search endpoint. Use Opportunities API search for q=.
My Opportunities at /opportunities/my is the owner dashboard (Edit / Delete / View). Saved and Applied tab counts are hardcoded 0 today — do not treat them as live metrics.
job, offer, request, partnership; keep Matcher auto-approve off until you trust scoring.collective_order, scheduled_services, asset_rental, bounty, tender with Wallet / PaymentConductor rails.ring_customization and program so customization quests and institution programs land in CRM.opportunities.opportunities.enabledTypes in ring-config.json (do not assume every OpportunityType union member is UI-visible).LLM_PROVIDER, OPENAI_API_KEY or ANTHROPIC_API_KEY, LLM_MODEL) before expecting auto-fill / LLM explanations./opportunities/my (ROUTES.MY_OPPORTUNITIES) — legacy /opportunities/my-opportunities is removed.| Viewer like / save / hide |
get-viewer-interactions.ts ← likes + user_content_interactions |
| Tag / chat provenance | lib/opportunity-tags.ts (splitOpportunityTags) |
| Feed card / list | opportunity-feed-card.tsx, opportunity-list.tsx |
| Interactions | app/_actions/opportunity-interactions.ts |
| Matcher core | lib/ai/matcher.ts + OpportunityMatchingService |
| Auto-fill | features/opportunities/services/auto-fill-service.ts |
| Auto-approve | features/opportunities/services/auto-approval-service.ts |
| PG JSONB ↔ client | features/opportunities/lib/opportunity-db-mapper.ts |
| Post-mutation sync | features/opportunities/lib/opportunity-mutation-sync.ts → syncOpportunityDiscovery |
| Program → CRM | features/opportunities/lib/program-crm-ingest.ts |
| UI Server Actions | app/_actions/opportunities.ts |
| Firestore legacy | lib/converters/opportunity-converter.ts when DB_BACKEND_MODE=firebase-full only |
Database: default production clones use DB_BACKEND_MODE=k8s-postgres-fcm (also supabase-fcm). Services return SerializedOpportunity (ISO date strings). Do not invent DATABASE_MODE=firebase_only.
(protected)/opportunities/page.tsx and GET /api/opportunities pass viewerUserId: session.user.id into getOpportunitiesForRole. Hidden ids are loaded first, then the query uses DatabaseService operator not-in on opportunities.id. PostgreSQLAdapter maps opportunities / likes id to the SQL column (not data->>'id').
attachOpportunityFeedFields adds creator, viewer, and likes. Cards hydrate useOptimistic from opportunity.viewer (the previous constant-base optimistic state never showed saved/liked). After Like / Save / Hide, onInteractionChange patches the cursor-feed item so session flags stay consistent.
useCursorFeed persists 24h in localStorage. The list fingerprint is …|viewer=<userId> so one account cannot inherit another’s like/save/hide. Reloads overlay server initialItems onto cached rows by id (hooks/use-cursor-feed.ts).
Browse useRealtimeOpportunities({ autoConnect: true }) stays for silent tunnel subscribe. Create snippets include creator { id, name, avatar } so live-inserted cards are not “Private User”. Detail (opportunity-details.tsx) still renders the Live Updates bar.
convertTaskToOpportunity writes metadata.source: { kind: 'chat_task', messageId, conversationId } and tags: []. splitOpportunityTags still parses legacy sourceMessageId: / sourceConversationId: / chat_task tags at render. There is no backfill job.
public | subscriber | member | confidentialdraft | pending | active | closed | expired | archivedcanCreateOpportunityConfidential / canEditOpportunity)| Bucket | Types (examples) | Minimum role |
|---|---|---|
| Member offers | offer, job, partnership, volunteer, mentorship, resource, event, ring_customization, program, collective_order, tender, asset_rental | member privileges |
| Requests | request | subscriber+ |
| Subscriber specials | cv, scheduled_services, bounty | subscriber+ |
| Other union members | e.g. future-facing types not in enabled/permission sets | admin+ (and must be enabled) |
Route contracts, create body, and list JSON live on Opportunities API. Like / Save / Hide / Contact are Server Actions (app/_actions/opportunity-interactions.ts), not REST. MCP GET /api/mcp/v1/opportunities currently does not pass viewerUserId.
After mutations, syncOpportunityDiscovery invalidates cache tags, revalidatePath for list/detail/my, and publishes Tunnel opportunity:*. That does not bust client useCursorFeed — overlay + viewer fingerprint are the browse freshness path.
Keys documented in env.local.template: LLM_PROVIDER, OPENAI_API_KEY, ANTHROPIC_API_KEY, LLM_MODEL, MAX_MATCHES_PER_OPPORTUNITY, MATCHING_MAX_TOKENS. Without a working provider, Matcher falls back to non-LLM / tag-style explanations with lower confidence.
Same-workflow: organization linkage and verified posters for opportunities.
/opportunities)Authenticated members see compact cards: type chip, title, two-line description, poster byline, relative posted time, human tags, like count, and icon actions.
| Action | Who | What happens |
|---|---|---|
| Like | Signed-in viewer | Toggles the likes collection; heart fills from opportunity.viewer.liked |
| Save | Signed-in viewer | Toggles user_content_interactions (save); hydrates from viewer.saved |
| Hide | Signed-in viewer | Marks not_interested; in-session Hidden · Undo; next list query excludes the id |
| Contact | Subscriber+; hidden on own posts | Navigates immediately to /messages?user=<createdBy>; Matcher contact_intent is best-effort and must not gate navigation |
| From chat | Owner only, when the listing came from a task | Opens /messages?c=<conversationId> |
There is no anonymous-posting flag. Authenticated viewers see the poster name/avatar. Username appears only on public profiles. Empty name renders Member vs Private User.
Right-rail query params (q, types, location, …) filter the current page of cards in the browser. GET /api/opportunities still paginates with limit / startAfter only — it is not a full-text search endpoint. Use Opportunities API search for q=.
My Opportunities at /opportunities/my is the owner dashboard (Edit / Delete / View). Saved and Applied tab counts are hardcoded 0 today — do not treat them as live metrics.
job, offer, request, partnership; keep Matcher auto-approve off until you trust scoring.collective_order, scheduled_services, asset_rental, bounty, tender with Wallet / PaymentConductor rails.ring_customization and program so customization quests and institution programs land in CRM.opportunities.opportunities.enabledTypes in ring-config.json (do not assume every OpportunityType union member is UI-visible).LLM_PROVIDER, OPENAI_API_KEY or ANTHROPIC_API_KEY, LLM_MODEL) before expecting auto-fill / LLM explanations./opportunities/my (ROUTES.MY_OPPORTUNITIES) — legacy /opportunities/my-opportunities is removed.| Viewer like / save / hide |
get-viewer-interactions.ts ← likes + user_content_interactions |
| Tag / chat provenance | lib/opportunity-tags.ts (splitOpportunityTags) |
| Feed card / list | opportunity-feed-card.tsx, opportunity-list.tsx |
| Interactions | app/_actions/opportunity-interactions.ts |
| Matcher core | lib/ai/matcher.ts + OpportunityMatchingService |
| Auto-fill | features/opportunities/services/auto-fill-service.ts |
| Auto-approve | features/opportunities/services/auto-approval-service.ts |
| PG JSONB ↔ client | features/opportunities/lib/opportunity-db-mapper.ts |
| Post-mutation sync | features/opportunities/lib/opportunity-mutation-sync.ts → syncOpportunityDiscovery |
| Program → CRM | features/opportunities/lib/program-crm-ingest.ts |
| UI Server Actions | app/_actions/opportunities.ts |
| Firestore legacy | lib/converters/opportunity-converter.ts when DB_BACKEND_MODE=firebase-full only |
Database: default production clones use DB_BACKEND_MODE=k8s-postgres-fcm (also supabase-fcm). Services return SerializedOpportunity (ISO date strings). Do not invent DATABASE_MODE=firebase_only.
(protected)/opportunities/page.tsx and GET /api/opportunities pass viewerUserId: session.user.id into getOpportunitiesForRole. Hidden ids are loaded first, then the query uses DatabaseService operator not-in on opportunities.id. PostgreSQLAdapter maps opportunities / likes id to the SQL column (not data->>'id').
attachOpportunityFeedFields adds creator, viewer, and likes. Cards hydrate useOptimistic from opportunity.viewer (the previous constant-base optimistic state never showed saved/liked). After Like / Save / Hide, onInteractionChange patches the cursor-feed item so session flags stay consistent.
useCursorFeed persists 24h in localStorage. The list fingerprint is …|viewer=<userId> so one account cannot inherit another’s like/save/hide. Reloads overlay server initialItems onto cached rows by id (hooks/use-cursor-feed.ts).
Browse useRealtimeOpportunities({ autoConnect: true }) stays for silent tunnel subscribe. Create snippets include creator { id, name, avatar } so live-inserted cards are not “Private User”. Detail (opportunity-details.tsx) still renders the Live Updates bar.
convertTaskToOpportunity writes metadata.source: { kind: 'chat_task', messageId, conversationId } and tags: []. splitOpportunityTags still parses legacy sourceMessageId: / sourceConversationId: / chat_task tags at render. There is no backfill job.
public | subscriber | member | confidentialdraft | pending | active | closed | expired | archivedcanCreateOpportunityConfidential / canEditOpportunity)| Bucket | Types (examples) | Minimum role |
|---|---|---|
| Member offers | offer, job, partnership, volunteer, mentorship, resource, event, ring_customization, program, collective_order, tender, asset_rental | member privileges |
| Requests | request | subscriber+ |
| Subscriber specials | cv, scheduled_services, bounty | subscriber+ |
| Other union members | e.g. future-facing types not in enabled/permission sets | admin+ (and must be enabled) |
Route contracts, create body, and list JSON live on Opportunities API. Like / Save / Hide / Contact are Server Actions (app/_actions/opportunity-interactions.ts), not REST. MCP GET /api/mcp/v1/opportunities currently does not pass viewerUserId.
After mutations, syncOpportunityDiscovery invalidates cache tags, revalidatePath for list/detail/my, and publishes Tunnel opportunity:*. That does not bust client useCursorFeed — overlay + viewer fingerprint are the browse freshness path.
Keys documented in env.local.template: LLM_PROVIDER, OPENAI_API_KEY, ANTHROPIC_API_KEY, LLM_MODEL, MAX_MATCHES_PER_OPPORTUNITY, MATCHING_MAX_TOKENS. Without a working provider, Matcher falls back to non-LLM / tag-style explanations with lower confidence.
Same-workflow: organization linkage and verified posters for opportunities.