Skip to content

Job Board

Status: Phase 0 scaffoldedapps/jobboard Axum service boots, jobboard.proto wired into the build. Auth = Supabase (JWT sub = auth.users.id); the Phase-0 first-party auth/sessions in the Rust code is a dead end to be removed. Schema migration shipped (20260615120000_jobboard_tables.sql): 23 tables, ULID-as-uuid PKs, junction tables (no array refs), state/money/lifecycle CHECK constraints, RLS lockdown, updated_at triggers; all user refs → auth.users(id). No seed yet. Phases 1–10 below remain spec.

A curated, both-sides-vetted job board for freelance work — think Upwork/Fiverr, but gated and quality-first. Sanctioned members post gigs, other sanctioned members take them up, and the platform handles discovery, matching, and messaging. Money moves off-platform in v1 (lead-gen model); monetization is built behind abstraction hooks to switch on later.

Game development is the launch vertical, not the ceiling. We pioneer in game dev (art, code, audio, design, QA for games) because it’s an underserved, portfolio-driven niche where curation wins — but the data model, taxonomy, and matching are vertical-agnostic by design. Adding a new vertical (e.g. video production, music, dev/IT, design, writing) is a data operation (seed a Vertical + its taxonomy), not a schema change or code change.

Architecture is decoupled: a standalone Rust/Axum REST API (own DB access, own first-party auth — no third-party auth SaaS) serving a separate frontend client (framework TBD) over HTTP. This keeps the API reusable across web, future mobile, and integrations.

  • Curation is the wedge. Both posters and takers are vetted, so quality is the product. Every gig is legit; every applicant is screened.
  • Vertical-first go-to-market, horizontal data model. We win one vertical at a time (game dev first) by speaking its language, while the underlying platform stays general so we can expand without re-platforming.
  • Portfolio-first. Talent in creative/technical verticals is hired by seeing work (characters, gameplay clips, playable builds, reels, repos), not reading résumés. The portfolio system is a first-class feature across every vertical.

PersonaWhat they doVetting
PosterHas work (indie dev, studio, publisher, agency, business). Posts gigs, reviews applicants, selects talent.Vetted (org/identity + intent)
TakerDoes work (artist, programmer, audio, designer, QA, and every other vertical’s specialists). Builds a portfolio, applies to gigs.Vetted (portfolio review)
AdminPlatform staff. Reviews membership applications, moderates gigs, manages verticals + taxonomy, handles reports.Internal
  • A single account can hold both capabilities (poster and taker), granted independently during vetting.
  • Capabilities are the authorization primitive — encoded as a bitmask on the existing User.role field (see protobuf section).
  • A member can operate in one or more verticals; their granted disciplines/skills are scoped per vertical.

The platform is organized around verticals (top-level service areas) and a controlled, admin-managed vocabulary scoped to each vertical — so filtering and matching work without free text, and so a new vertical can be launched without touching code.

  • Vertical — the top-level service area. Game Dev is vertical #1. Others (Video, Audio/Music, Software/IT, Design, Writing, Marketing…) are added later as data.
  • Discipline — a category of work within a vertical (data-driven taxonomy, not a hardcoded enum).
  • Tool/Platform — engines, software, frameworks relevant to the vertical.
  • Skill tag — finer-grained, controlled-but-extensible.

Why no hardcoded discipline enum? An earlier draft baked game-dev disciplines into a protobuf bitmask. That ties the schema to one vertical. Instead, disciplines/tools/skills all live in the Taxonomy table, each row scoped by vertical_id. Gigs and profiles reference taxonomy ids, so a new vertical is purely a seed-data operation. (If a vertical later needs ultra-fast bitmask filtering, a per-vertical cached bitmask can be derived from taxonomy ids without changing the contract.)

Game-dev vertical — seed vocabulary (example)

Section titled “Game-dev vertical — seed vocabulary (example)”

This is the first vertical’s seed data, illustrating the pattern — not a fixed part of the schema:

  • Disciplines: 2D Art, 3D Art, Animation/Rigging, Programming, Technical Art/Shaders, Audio (Music/SFX/VO), Game Design, Level Design, Narrative/Writing, UI/UX, QA/Testing, Porting, Localization, Production/PM, Community.
  • Tools/Engines: Unity, Unreal, Godot, GameMaker, Bevy, Custom; tools — Blender, Maya, ZBrush, Substance, Photoshop, Spine, FMOD, Wwise.
  • Skill tags: netcode, humanoid-rigging, pixel-art, procgen, shader-graph, vertical-slice.

Admin-managed vocabulary so new verticals, disciplines, tools, and skills can be added without code changes.


  1. Sign up (first-party auth — email/password or OAuth).
  2. Choose intended capabilities: post work, take work, or both; and the vertical(s) of interest.
  3. Complete application — statement + portfolio links; talent pick vertical(s) + disciplines/tools, posters add org info.
  4. Submit → application status pending.
  5. Admin reviews in the vetting queue → approve/reject per capability.
  6. On approval → capability bit set on User.role; user unlocks profile + the granted actions.
  1. Approved poster creates a gig (title, vertical, discipline(s), scope, budget, tools, skills, deadline).
  2. Publish → OPEN (optionally PENDING_REVIEW if gig moderation is enabled).
  3. Gig enters discovery.
  1. Browse / search / filter gigs (vertical, discipline, tool, budget…).
  2. View gig detail.
  3. Apply — cover message, proposed rate, attach relevant portfolio items (gated on taker).
  4. Poster notified.
  1. Poster reviews applicants, shortlists, messages candidates.
  2. Poster accepts one → Engagement created, gig → FILLED, others auto-declined/notified.
  3. Work + payment happen off-platform (v1).
  4. Either party marks the engagement complete.
  5. Both leave reviews → reputation accrues (aggregated into User.reputation).
  • Vetting queue, gig takedowns, report handling, vertical + taxonomy management, audit log.

Accessed via sqlx (async, compile-time-checked queries; migrations via sqlx migrate). Each table is mirrored by a protobuf message (see below). Reused entities map to existing protos; new entities to the jobboard package.

TableSource proto
auth.users (identity)Supabase — referenced by user_id FKs, not recreated
profiles (base)reuse Profile (kbveproto)
verticalsnew jobboard.Vertical
talent_profilesnew jobboard.TalentProfile
client_profilesnew jobboard.ClientProfile
member_applicationsnew jobboard.MembershipApplication
portfolio_itemsnew jobboard.PortfolioItem (+ reuse MediaAttachment)
taxonomy (disciplines/tools/skills, scoped per vertical)new jobboard.Taxonomy
gigsnew jobboard.Gig
applicationsnew jobboard.Application
engagementsnew jobboard.Engagement
reviewsnew jobboard.Review
conversations, messagesnew jobboard.Conversation / Message (+ reuse MediaAttachment)
notificationsnew jobboard.Notification
reports, audit_lognew (admin)
invoices, settings, globals (monetization hooks)reuse Invoice, Setting, Global (kbveproto)

ConcernChoiceWhy
FrameworkAxum (Tokio)Fast, ergonomic, tower middleware ecosystem
AuthSupabase — verify the Supabase JWT (sub = auth.users.id, kbve_username claim), no first-party user/session storeReuses existing KBVE identity; same model as apps/rows
DB accessjedi PgCluster (tokio-postgres + bb8, rw/ro/any split, RLS caller-scoped reads, valkey cache)House pattern (matches axum-kbve); free read/write split + RLS + caching
Migrationsdbmate (packages/data/sql/dbmate/migrations) — schema deferred, not committed yetRepo-standard migration tool; one system across the monorepo
DBPostgres — KBVE CNPG/Supabase cluster via KBVE_PG_RW_URLShared cluster; jobboard schema isolates the tables
Wire formatprost/tonic protobuf over jedi.JediEnvelopeReuses the KBVE proto/envelope transport
MediaS3-compatible (Cloudflare R2)Rust-friendly; private deliverables
SearchPostgres FTS (v1) → Meilisearch laterMeilisearch is Rust-native, easy upgrade
NotificationsIn-app (DB) v1; email laterKeep v1 lean

Frontend — TBD (decided in a later phase)

Section titled “Frontend — TBD (decided in a later phase)”
  • Pure HTTP client of the API. Candidates: Astro + React (KBVE-native), Next.js (frontend-only), or React SPA.
  • Auth carried as httpOnly session cookie (browser) — already supported by the API design.

Authorization model: Axum middleware/extractors authenticate the session/token; route guards check the Capability bitmask on User.role (POSTER/TAKER/ADMIN); ownership checks (e.g. gig owner) enforced per-handler.


The deployable surface lives in apps/jobboard: Dockerfile (chiseled Ubuntu axum base), version.toml, and nx project.json (build / test / lint / run / container / containerx). The container build context is the repo root.

  • Built via ./kbve.sh -nx jobboard:containerkbve/jobboard:latest (+ version tag); production config also tags ghcr.io/kbve/jobboard.
  • Multi-stage cargo-chef build (planner → cook → builder → runtime) on ghcr.io/kbve/chisel-ubuntu-axum. Runtime image runs /app/jobboard as uid 10001, exposes 5400.
  • Protos are vendored (apps/jobboard/src/proto/jobboard.rs); the image build does not run protoc. Regenerate only when the .proto changes: BUILD_PROTO=1 cargo build -p jobboard (and revert the incidental jedi proto regen before committing).
VarRequiredDefaultNotes
KBVE_PG_RW_URLyesjedi PgCluster primary (rw). DATABASE_URL is mapped to it if unset (dev convenience).
KBVE_PG_RO_URLno= rwRead replica; falls back to rw.
KBVE_PG_ANY_URLno= rwAny-instance read fallback.
HTTP_HOSTno0.0.0.0Bind host.
HTTP_PORTno5400Bind port.
SECURE_COOKIESnofalseSet true behind TLS (sets Secure on the session cookie).
RUST_LOGnojobboard=info,tower_http=infoTracing filter.

jedi PgCluster also honors its standard KBVE_PG_* tuning knobs (pool sizes, timeouts, search_path, pooler rewrite, CA bundle) — see packages/rust/jedi/src/state/pg.rs. TLS bypasses transparently when the URL carries sslmode=disable (in-cluster), engages for sslmode=require (external).

  • Liveness: GET /health (always 200 when the process is up).
  • Readiness: GET /ready (200 when the DB cluster health probe passes, 503 otherwise) — gate traffic on this.

Deployed under apps/kube/jobboard/ into the kbve namespace (own ArgoCD app):

  • jobboard-deployment.yaml — Deployment (ghcr.io/kbve/jobboard:<ver>, port 5400, liveness /health, readiness /ready, Reloader) + jobboard-service (ClusterIP 5400). Reuses the kbve-ns supabase-shared secret for KBVE_PG_RW_URL/_RO/_ANY (no new ExternalSecret).
  • httproute.yamljobs.kbve.comjobboard-service:5400, parented to kbve-gateway. No cert/ReferenceGrant/gateway-listener needed: jobs.kbve.com is covered by the existing https-kbve wildcard listener (*.kbve.com, cert kbve-wt-tls).
  • jobboard-serviceaccount.yamljobboard-sa, automountServiceAccountToken: false. No RBAC Role/RoleBinding — the pod never calls the k8s API.
  • application.yaml registered in apps/kube/kustomization.yaml (kube-root app-of-apps).
  • ArgoCD apps track main; PRs land on dev, so the manifest reaches the cluster only after the periodic dev→main release.

The frontmatter above (version, image, version_toml, version_target, deployment_yaml) registers jobboard in .github/ci-dispatch-manifest.json (regenerate via astro-kbve:buildastro-kbve:sync:ci-manifest; registry entry in @kbve/devops registry.ts). On a version: bump here, ci-docker.yml’s version gate (MDX version > version.toml) builds + pushes ghcr.io/kbve/jobboard:<ver>, then utils-post-publish.yml opens an atom PR that sed-updates version.toml, Cargo.toml, and the deployment image tag in jobboard-deployment.yaml. First publish uses the version.toml = 0.0.1 sentinel so the gate fires.

Source of truth: packages/data/sql/schema/jobboard/jobboard.sql — the canonical desired-state DDL (same convention as schema/meme, schema/ows, etc.). The dbmate migration packages/data/sql/dbmate/migrations/20260615120000_jobboard_tables.sql is the applied delta that brings prod to that state, generated from / kept identical to the schema file. The proto (packages/data/proto/jobboard/jobboard.proto) is the third leg and tracks the same shape. 23 tables, hardened with constraints, junction tables, RLS lockdown, and updated_at triggers. No seed (separate follow-up migration).

  • Identity is Supabase. No jobboard.users / jobboard.sessions — every owner column is user_id uuid references auth.users(id). The service trusts the Supabase JWT (sub); reputation/capabilities live on jobboard rows or are derived.
  • Primary keys: lookup tables (verticals, taxonomy) use bigint generated always as identity; entity tables use ULID-as-uuiduuid default public.gen_ulid()::uuid (the house generator’s 32-hex output casts to a 16-byte binary uuid whose time prefix makes it chronologically sortable → non-enumerable + cheap keyset feeds). user_id is uuidauth.users.
  • Junction tables replace all *_ids arrays (real FKs + uniqueness): talent_verticals, talent_taxonomy, client_verticals, member_application_verticals, portfolio_tags, gig_taxonomy, application_portfolio_items. Single-vertical entities (gig_taxonomy, portfolio_tags) carry vertical_id and use composite FKs to gigs(id, vertical_id) / portfolio_items(id, vertical_id) + taxonomy(id, vertical_id), so a tag must belong to the entity’s own vertical (cross-vertical tagging is rejected). talent_taxonomy is also vertical-aligned — composite FKs to talent_verticals(user_id, vertical_id) + taxonomy(id, vertical_id), so a talent can only tag taxonomy within a vertical they’ve joined.
  • State integrity: CHECK constraints on every enum/bitmask column (ranges from the proto enums — budget_type 0–3; gig/notification status/kind in (0,1,2,4,8,16) treated as single-value enums; reports.target_kind in (1..4) = ReportTarget, reports.status in (0..3) = ReportStatus), money invariants (min ≤ max, ≥ 0, currency ~ '^[A-Z]{3}$', minor units), identifier/text rules (slug/name ^[a-z0-9]+(-[a-z0-9]+)*$ — no leading/trailing/double hyphens; non-empty bounded label/title/reason/action), lifecycle invariants (member-app review state, one-pending-per-user partial unique, gig publication state — OPEN/closed states require published_at, engagement status↔completed_at lifecycle [COMPLETED⇒completed_at ≥ started_at, else null] + one-per-gig, null-safe distinct-parties / distinct-reviewer-reviewee so double account deletion can’t trip the check, participant left_at ≥ joined_at), unique review-per-(engagement, reviewer), message content non-empty + size caps (body ≤ 20k, ≤ 10 attachments), jsonb_typeof checks.
  • Messaging: conversation_participants has a surrogate id PK; messages reference the participant via composite FK (conversation_id, sender_participant_id) → conversation_participants(conversation_id, id) with ON DELETE NO ACTION — a participant row can’t be hard-deleted while it has messages (participants leave by setting left_at, never by row deletion), yet a full conversation delete still cascades cleanly (NO ACTION defers to statement end; RESTRICT would break that). On account deletion the participant’s user_id is SET NULL (anonymized) and messages are preserved (they key off the immutable participant id, not the auth uuid). Per-participant read cursor (last_read_message_id, conversation-scoped composite FK with column-list SET NULL), not a single messages.read_at.
  • Indexes are feed/keyset-oriented: partial gig feeds where status = 2 (vertical_id, published_at desc, id desc + global), applicant/poster application queues, message cursor, partial unread-notifications, reports queue, reviewee history, portfolio ordering (user_id, sort_order, created_at desc, id desc), member-application history + moderation queue, active-participant partial (user_id, conversation_id where left_at is null), and reverse indexes on every junction’s non-leading column.
  • Bounds + self-documentation: length caps on all free text (headline/org_name≤200, about≤2000, descriptions≤5–20k, cover_message/body≤10k, portfolio_links ≤20) and comment on column on every enum/bitmask column (GigStatus, EngagementStatus, ReportTarget, …) so the integer↔meaning mapping lives in the catalog and can’t silently drift from the proto/Rust enums.
  • Delete policy (middle ground): personal data CASCADE (profiles, portfolio, notifications, member applications, conversation participants); commercial/moderation history SET NULL + nullable (gigs.poster_id, applications.applicant_id, engagements, reviews, reports, audit_log, and conversation_participants.user_id) so the marketplace + chat record survives account deletion. Snapshot/display-name capture deferred.
  • RLS / privileges locked to a service-role-only model (the Rust service reaches the schema via its privileged jedi pool, NOT via PostgREST/authenticated): create schema jobboard authorization postgres; RLS enabled on all 23 tables; force on the strictly server-mediated tables (engagements, reviews, member_applications, reports, audit_log); revoke all on all tables/sequences from anon, authenticated; role-specific alter default privileges for role postgres … revoke (the generic form only covers the running role); schema revoke from public with no authenticated usage grant — so authenticated gets permission denied for schema jobboard (verified). The set_updated_at() fn is revoke … from public + owner to postgres. No plpgsql/PostgREST RPC layer — the jobboard Axum service is the single writer and reaches the schema via its jedi pool connecting as a BYPASSRLS role (service_role or postgres), which is required because force row level security binds even the table owner (superusers/BYPASSRLS bypass it). RLS + force therefore act purely as defense-in-depth against any future direct/PostgREST path; the real authorization + multi-step invariants live in the service. (If direct client reads are ever wanted, add PostgREST grants + SELECT policies, or public.* proxy wrappers, per feature phase.)
  • Verified up + down on Postgres 17 (stub auth.users/gen_ulid): 23 tables, 60 indexes, schema owner postgres; negative battery confirms cross-vertical tag, non-joined talent taxonomy, bad slug, empty label, over-length text, >20 portfolio links, unpublished-OPEN gig, COMPLETED-without-completed_at, dup review, empty message, cross-conversation cursor, bad target_kind/target_id, and authenticated schema access all reject — while a full conversation delete cascades and a direct participant delete (with messages) is blocked, preserving history.

Enforced in the jobboard Axum service, not DDL (nullable historical FKs + temporal/identity rules can’t be plain CHECKs). The service is the single writer; each multi-step invariant is a Rust transaction (conn.transaction() + SELECT … FOR UPDATE on the gig/application rows), with the DB constraints as the safety net underneath — no plpgsql RPCs: portfolio-item ownership on application attach (portfolio.user_id = application.applicant_id); engagement creation (lock gig + accepted application, verify caller, reject competing applications, flip gig status, audit — one tx); reviews must have reviewer/reviewee ∈ engagement parties, reviewer ≠ reviewee, engagement COMPLETED; non-null identities on insert (the nullable id columns exist only for ON DELETE SET NULL, never for fresh rows); message send must reject left_at is not null participants (and optionally cursor-before-join); member-application review transitions; reports moderation; capability/role grants.

Other deferred follow-ups: seed migration; per-phase RLS SELECT policies; conversation inbox denormalization (conversations.last_message_at/id, maintained in the send RPC); attachment element-type validation (array-of-objects, RPC/helper); proto entity id fields uint64 → bytes (uuid) + Message.sender → participant id. The down (drop schema … cascade) is bootstrap-only — mark irreversible once prod data exists.


All mutations go through the API; the frontend is a pure client. Auth via session cookie or Authorization: Bearer. Versioned under /api/v1.

# Auth
POST /auth/register email/password signup
POST /auth/login -> session cookie / token
POST /auth/logout
GET /auth/me current user + capabilities
GET /auth/oauth/:provider start Discord/GitHub/Steam flow
GET /auth/oauth/:provider/cb callback
# Verticals & taxonomy
GET /verticals list active verticals
GET /verticals/:id/taxonomy disciplines/tools/skills for a vertical
POST /admin/verticals [admin] create vertical
POST /admin/taxonomy [admin] add discipline/tool/skill
# Membership / vetting
POST /applications submit membership application
GET /admin/applications [admin] vetting queue
POST /admin/applications/:id/decision [admin] approve/reject per capability
# Profiles & portfolio
GET /talent browse/filter talent (vertical, discipline, tool…)
GET /talent/:username
PATCH /me/profile
GET/POST/PATCH/DELETE /me/portfolio[/:id]
# Gigs
GET /gigs browse/filter (vertical, discipline, tool, budget…)
POST /gigs [poster] create
GET /gigs/:id
PATCH /gigs/:id [poster, owner]
POST /gigs/:id/close [poster, owner]
# Applications to gigs
POST /gigs/:id/applications [taker] apply
GET /gigs/:id/applications [poster, owner] list applicants
PATCH /applications/:id shortlist/decline/withdraw
POST /applications/:id/accept [poster] -> creates engagement, fills gig
# Messaging
GET /conversations
GET /conversations/:id/messages
POST /conversations/:id/messages
# Engagements & reviews
POST /engagements/:id/complete
POST /engagements/:id/reviews
# Moderation
POST /reports
GET /admin/reports [admin]

The platform’s wire/storage contract is protobuf. The proto root is packages/data/proto/ (the shared protoc include root — cross-package imports already work there). We add one new file — packages/data/proto/jobboard/jobboard.proto (package jobboard).

Phase 0 reality check. The reuse table below was aspirational; the actual repo differs, and Phase 0 ships jobboard.proto as standalone (it defines its own MediaAttachment rather than importing one) to sidestep cross-package include friction. Corrections to the table: identity protos live in packages/data/proto/kbve/kbveproto.proto with no package declaration (so they are awkward to import into a packaged file — reuse happens at the DB/service layer, not via proto import); there is no status.proto (MediaAttachment was invented locally; the closest existing is kbve.forum.Attachment); the Discord listing template is kbve/discordsh.proto (DiscordServer + ServerTag), not disoxide.proto; there is no store.proto/StoreService (only jedi/redis.proto’s RedisService). JediEnvelope/MessageKind/PayloadFormat (jedi/jedi.proto) and RedisService are real and reusable. As later phases need true cross-file reuse, give kbveproto.proto a package and import from the shared root.

NeedReused protoWhere it livesNotes
Auth / credentialsAuthkbveproto.protoAlready has email, hash, salt, reset token+expiry, verification token+expiry, status, last_login, failed_login_attempts, lockout_until, 2FA secret, recovery codes. This is our first-party auth model — use as-is.
User identityUserkbveproto.protouserid, username, role, reputation, exp, created_at. We encode capabilities into role (bitmask) and aggregate review scores into reputation.
Base social profileProfilekbveproto.protoname, bio, github, instagram, discord, unsplash, userid. Vertical-specific fields go in a new TalentProfile that references the same userid — we extend, not duplicate.
Bearer API tokensApikeykbveproto.protoFor non-browser clients (Authorization: Bearer).
Monetization hooksInvoice, Setting, Globalkbveproto.protoWired only when monetization switches on.
Media (portfolio/attachments)MediaAttachmentstatus.protourl, media_type (bitmask), caption. Embedded by PortfolioItem and Message.
Listing patternDiscordServer, DiscordTagdisoxide.protoTemplate (not copied): bitmask status/categories, inline SQL CHECK comments, URL regex, updated_at. Gig, Vertical, and Taxonomy follow this convention.
Transport envelopeJediEnvelope, MessageKind, PayloadFormatjedi.protoJob board messages travel as payload bytes inside JediEnvelope. Existing MessageKind bits (AUTH, HTTP_API, WEBSOCKET, REDIS, SUPABASE) already cover routing — no new kinds needed for v1.
KV / cache / realtimeStoreService, RedisServicestore.proto, redis.protoCaching, presence, live message/notification fan-out. Reuse as-is.

Canonical source: packages/data/proto/jobboard/jobboard.proto (package jobboard, standalone, built via prost). It is not inlined here — that drifts; the proto is kept in sync with the SQL schema package (packages/data/sql/schema/jobboard/).

  • Id conventions: entity ids are bytes = the 16-byte ULID-as-uuid (public.gen_ulid()::uuid); user refs are bytes = auth.users(id); lookup ids (Vertical, Taxonomy) and the taxonomy/vertical id lists are uint64 = bigint identity; timestamps are ISO string.
  • Messages mirror the tables: added ConversationParticipant, Report, AuditLog; Message carries sender_participant_id (not a raw user id) and dropped read_at (read state is the participant cursor); Conversation dropped participant_ids; ById keys on bytes, ByLookup on uint64.
  • Taxonomy/vertical id lists (discipline_ids/tool_ids/skill_ids/tag_ids/vertical_ids) stay repeated uint64 — the API view of the junction tables; relational integrity (same-vertical, ownership) lives in SQL.
  • New file: jobboard.proto (1 file, 1 package).
  • Reused as-is: JediEnvelope/MessageKind/PayloadFormat (jedi/jedi.proto), RedisService (jedi/redis.proto). (Identity reuse — Auth/User/Profile etc. — happens at the DB/service layer, not via proto import, since kbveproto.proto has no package; see the reality-check note above.)
  • Reused as pattern (not copied): kbve.discordsh.DiscordServerGig, ServerTagVertical/Taxonomy.
  • No new MessageKind values — existing routing bits suffice for v1.
  • No hardcoded discipline enum — disciplines/tools/skills are data-driven taxonomy scoped by vertical, so new verticals require no proto change.

  • Vetting on both sides is the primary trust mechanism.
  • Reporting + admin takedown for gigs, profiles, messages.
  • Off-platform payment disclaimer + safety guidance (no escrow yet — set expectations clearly).
  • Audit log for admin actions.

  1. Disintermediation / leakage — with payments off-platform, members can match once and transact off-platform forever. Mitigations: keep reviews/reputation valuable enough to bring people back; consider gating contact details until a stage; plan the monetization switch before leakage becomes habit.
  2. Vetting throughput — manual approval is the quality bar and a growth bottleneck. Needs a written rubric (per vertical) + a fast admin UI.
  3. Cold-start liquidity — curated + both-vetted = slowest to fill. Likely need to hand-seed one side first, in one vertical first. Game dev launches first; which side to seed?
  4. “Sanctioned” criteria undefined — what objectively qualifies a poster vs a taker, per vertical?
  5. Vertical expansion sequencing — the model supports many verticals, but each needs taxonomy seeding, a vetting rubric, and liquidity. Which vertical follows game dev, and on what trigger?
  6. Proto include paths — sharing MediaAttachment/identity across packages needs a common protoc include root; decide whether to relocate shared protos or vendor.

PhaseScopeOutcome
0 — FoundationsAxum + sqlx + Postgres; migrations; verticals + taxonomy seed (game dev first); first-party auth (register/login/session/logout) + CORS; wire jobboard.proto into the buildAPI boots, auth works, DB ready
1 — Identity & vettingMembership application endpoints, admin vetting queue, capability grants (per vertical)Members can be sanctioned
2 — Profiles & portfolioTalent/client profiles, portfolio CRUD + R2 media uploadPublic profiles exist
3 — GigsPost gig, gig moderation, browse + filter (vertical/discipline/tool), gig detailWork is discoverable
4 — Applications & selectionApply, applicant management, accept → engagementMatching works end-to-end
5 — MessagingConversations + threads + attachmentsParties can talk
6 — Engagements & reviewsMark complete, two-sided reviews, reputation displayTrust accrues
7 — Frontend clientChoose framework; build the UI against the APIUsable product
8 — Discovery & notificationsSearch polish, saved items, in-app notificationsRetention loops
9 — Monetization hooksWire entitlements to a chosen model (sub/featured/credits) via reused Invoice/SettingRevenue switch ready
10 — Second verticalSeed a new vertical (taxonomy + rubric); validate the model expands without schema changeMulti-vertical proven

Locked: curated, both-sides vetted · gig→apply mechanic · lead-gen v1 (off-platform payments) · monetization-agnostic hooks · game dev as the launch vertical, vertical-agnostic data model (verticals + data-driven taxonomy, no hardcoded discipline enum) · decoupled architecture · Rust/Axum API · Supabase auth (verify JWT, sub = auth.users.id; all jobboard rows FK auth.users) — first-party auth was a wrong turn, removing it · jedi PgCluster (tokio-postgres + bb8) + dbmate migrations against the KBVE cluster · standalone jobboard.proto (one package, standalone for Phase 0).

Still open:

  • Frontend framework (Astro+React / Next.js frontend-only / React SPA) — deferred to Phase 7.
  • Sanctioning rubric for posters and takers (per vertical).
  • Which side to seed first for liquidity (in the game-dev vertical).
  • Which vertical follows game dev, and on what trigger.
  • When/whether to gate contact info to reduce leakage.
  • Eventual monetization model (subscription / featured / credits).
  • cookie-only vs cookie+bearer; OAuth providers (Discord/GitHub/Steam) for v1.
  • Proto include-path strategy (shared root vs vendor).