Skip to content
TARGET-API

Target API plan

The catalogue below applies the conventions of ADR-011 to the target data model (data-model). Every one of today's 114 public procedures appears in the Today column of exactly one row (or in the "Removed" lists); the 13 admin procedures are summarised without names, as ADR-000 requires for public docs (the names are on /architecture/api/admin).

Auth tiers: public · authed (valid session) · verified (verified email, ADR-003) · member(roles) (a profile_members role on the profile in the input, ADR-004) · admin. +RL = rate-limited. Status: exists (kept as is) · rename · change (shape or behaviour) · new · remove. Paged inputs take { cursor?, limit? } and return { items, nextCursor }.

identity

TargetKindAuthInput → outputStoriesToday · status
identity.requestLinkmutationpublic +RL{ email, purpose: login or reset, signup? } → { sent }RAZ-241, map:meetup-2.1-sign-upauth.requestMagicLink · change (never writes dancers, hashed token)
identity.consumeLinkmutationpublic +RL{ token } → Session (sets HttpOnly cookie)RAZ-241, map:meetup-2.1-sign-upauth.verifyMagicLink · change
identity.resetPasswordmutationpublic +RL{ token, newPassword } → SessionRAZ-241auth.resetPassword · change (purpose-bound, revokes sessions)
identity.signInmutationpublic +RL{ email, password } → Sessionmap:meetup-2.1-sign-upauth.login · rename
identity.registermutationpublic +RL{ name, email, password, styles, partnerRole?, cityId? } → Session + verify emailmap:meetup-2.1-sign-upauth.register · change
identity.verifyEmailmutationpublic{ token } → { verified }RAZ-83, map:meetup-2.1-sign-up— · new
identity.signOutmutationauthed{} → { ok } (revokes session)map:meetup-2.1-sign-up— · new
identity.changePasswordmutationauthed +RL{ currentPassword, newPassword } → { ok }none (see below)auth.changePassword · change (revokes other sessions)
identity.mequerypublic{} → Me or nullmap:meetup-2.1-sign-upauth.me · rename
identity.completeOnboardingmutationauthed{ intent, cityId?, styles?, partnerRole? } → { ok }RAZ-29, map:meetup-2.1-sign-upauth.completeOnboarding · change (cityId)
identity.deleteAccountmutationauthed{} → { deleted }RAZ-155auth.deleteAccount · change (FK rules do the work)

dancers

TargetKindAuthInput → outputStoriesToday · status
dancers.getByHandlequerypublic{ handle } → DancerPublic (+ approved videos)RAZ-154profile.getByUsername · rename
dancers.updateMemutationauthedpartial { name, cityId, styles: [{ key, level }], partnerRole, photoUrl, bio, socials, profilePublic } → { ok }RAZ-249, RAZ-275profile.update · change

profiles

TargetKindAuthInput → outputStoriesToday · status
profiles.getByHandlequerypublic{ handle } → Profile + spaces + lineup rolesRAZ-66, RAZ-303, RAZ-253, RAZ-135, RAZ-159entity.getByHandle · rename
profiles.listquerypublicpaged { type, cityId?, roles?, styles?, amenities?, q? } → ProfileCard[]RAZ-50, RAZ-58, RAZ-64, RAZ-66, RAZ-303, RAZ-45, RAZ-129, RAZ-302entity.listArtists, entity.listVenues, entity.listByCity · change (merged, paged, filtered)
profiles.updatemutationmember(owner, editor){ profileId, …fields } → { ok }RAZ-303, RAZ-159— · new
profiles.claimmutationverified +RL{ profileId, evidence } → { claimId }RAZ-253, RAZ-129— · new
profiles.members.listquerymember(owner){ profileId } → Member[]RAZ-129, RAZ-158— · new
profiles.members.invitemutationmember(owner){ profileId, email, role } → { ok }RAZ-129, RAZ-158— · new
profiles.members.removemutationmember(owner){ profileId, dancerId, role } → { ok }RAZ-129— · new
profiles.affiliations.setmutationmember(owner, editor){ artistProfileId, organizerProfileId, kind } → { ok }RAZ-66— · new
profiles.followmutationauthed{ profileId } → { following: true }RAZ-55follow.toggle · change (idempotent pair)
profiles.unfollowmutationauthed{ profileId } → { following: false }RAZ-55follow.toggle · change
profiles.followStatequerypublic{ profileId } → { following: boolean or null, followers }RAZ-55follow.isFollowing, follow.count · change (merged)
profiles.myFollowsqueryauthedpaged {} → ProfileCard[]RAZ-55follow.myFollows · rename

directory

TargetKindAuthInput → outputStoriesToday · status
directory.cities.listquerypublicpaged {} → CityCard[] (counts, hero image)RAZ-139, RAZ-47entity.listCities, GET /api/cities · change (one source)
directory.cities.getquerypublic{ slug } → City + directory countsRAZ-45, RAZ-168, RAZ-169entity.cityDirectory, GET /api/cities/:slug · change
directory.cities.searchquerypublic +RL{ q, session } → CitySuggestion[]RAZ-85, RAZ-29GET /api/city-search · change (REST → tRPC)
directory.cities.resolvemutationpublic +RL{ placeId, session, query? } → CityRAZ-85, RAZ-29POST /api/city-resolve · change
directory.cities.optionsquerypublic{} → CityOption[]RAZ-85GET /api/city-options · change
directory.styles.listquerypublic{} → DanceStyle[]RAZ-25, RAZ-26, RAZ-27, RAZ-303— · new
directory.communityGroups.listquerypublic{ cityId } → CommunityGroup[]RAZ-156communityGroup.listByCity · rename
directory.communityGroups.reportmutationauthed +RL{ groupId } → { hidden }RAZ-156communityGroup.report · change (TRPCError codes)

events

TargetKindAuthInput → outputStoriesToday · status
events.listquerypublicpaged { cityId, from, to, kinds?, styles?, freeOnly? } → EventCard[]RAZ-28, RAZ-30, RAZ-71, RAZ-162, RAZ-168, RAZ-251, RAZ-29, RAZ-108, RAZ-113events.byCity, booking.upcomingByCity · change (one table, no pending bookings)
events.getquerypublic{ id or slug } → Event (+ lineup, festival details)RAZ-251, RAZ-304, RAZ-158events.get, booking.getEvent · change (merged)
events.byProfilequerypublicpaged { profileId, from, to } → EventCard[]RAZ-55, RAZ-161, RAZ-253, RAZ-158events.byProfile, booking.scheduleForProfile · change (FK, no requester data)
events.createmutationmember(owner, editor){ organizerProfileId, kind, title, startsAt, endsAt, timezone, venueProfileId?, styles, price… } → { id, slug }RAZ-126, RAZ-33— · new
events.updatemutationmember(owner, editor){ eventId, …fields } → { ok }RAZ-126— · new
events.cancelmutationmember(owner, editor){ eventId, reason? } → { ok }RAZ-126— · new
events.series.upsertmutationmember(owner, editor){ seriesId?, rrule, timezone, defaults… } → { seriesId }RAZ-126, RAZ-108— · new

festivals

TargetKindAuthInput → outputStoriesToday · status
festivals.listquerypublicpaged { from?, styles?, q? } → FestivalCard[] with going/verified countsRAZ-105, RAZ-131, RAZ-32, RAZ-56, RAZ-88GET /api/festivals · change (REST → tRPC via SSR caller)
festivals.getquerypublic{ slug } → Festival (details, lineup, ticket types)RAZ-105, RAZ-83, RAZ-36, RAZ-54festival.bySlug, GET /api/festivals/:slug · change
festivals.programmequerypublic{ eventId, style?, level? } → Session[] with artistsRAZ-36, RAZ-54, RAZ-97— · new (was app/data/mock-*.ts)
festivals.programme.upsertSessionmutationmember(owner, editor){ eventId, session } → { sessionId }RAZ-36, RAZ-141— · new
festivals.submitDraftmutationpublic +RL{ slug?, name?, email?, payload (bounded) } → { id }RAZ-95, RAZ-112, RAZ-117, RAZ-122, RAZ-126, RAZ-141festival.submitDraft · change
festivals.suggestionsquerypublic{ limit } → FestivalCard[] (people-you-met first when signed in)RAZ-38, map:meetup-8.3-next-festival-inviteplan.suggestions · change

ticketing

TargetKindAuthInput → outputStoriesToday · status
ticketing.ticketTypes.listquerypublic{ eventId } → TicketType[] with remaining capacityRAZ-69, RAZ-149, RAZ-110— · new (was ticket-prices.ts)
ticketing.ticketTypes.upsertmutationmember(owner, editor){ eventId, ticketType } → { id }RAZ-33, RAZ-144— · new
ticketing.checkoutmutationauthed +RL{ eventId, items: [{ ticketTypeId, quantity }], referralCode?, idempotencyKey } → { orderId, checkoutUrl or null, discountCents }RAZ-23, RAZ-110, RAZ-130, map:meetup-2.2-free-spot-counter, map:meetup-2.3-social-paywallfestivalSignup.ticketCheckout, festivalSignup.createCheckoutSession, festivalSignup.join · change (one checkout; free passes are 0-cent ticket types)
ticketing.availabilityquerypublic{ eventId } → { ticketTypes: [{ id, remaining }], mine?: { attendance, orderStatus } }map:meetup-2.2-free-spot-counter, RAZ-110, RAZ-248festivalSignup.status · change
ticketing.myOrdersqueryauthedpaged {} → Order[]RAZ-99, RAZ-110festivalSignup.myTickets · change (orders, any provider)
ticketing.orderqueryauthed{ orderId } → OrderRAZ-246— · new (confirmation from the server, not sessionStorage)
ticketing.referrals.validatequerypublic +RL{ eventId, code } → { referrerName, discountPercent } or nullRAZ-23referral.validate · rename
ticketing.referrals.minequeryauthed{ eventId? } → { referrals, availableCreditCents }RAZ-23referral.myReferrals · change (authed tier)

attendance

TargetKindAuthInput → outputStoriesToday · status
attendance.setmutationauthed{ eventId, status: interested or going or none } → AttendanceRAZ-136, RAZ-154, RAZ-125, RAZ-71, RAZ-38, RAZ-127plan.add, plan.remove (festival/event rows) · change
attendance.minequeryauthed{ eventId } → Attendance or nullRAZ-115, RAZ-83plan.getShareGoing, festivalSignup.getMyVisibility · change (merged)
attendance.setVisibilitymutationauthed{ eventId, visibility } → { visibility }RAZ-115, RAZ-34, RAZ-83plan.setShareGoing, festivalSignup.setMyVisibility · change (merged)
attendance.updateLogisticsmutationauthed{ eventId, partnerStatus?, travelBooked?, stayBooked?, vacationBooked? } → { ok }RAZ-80, RAZ-75, RAZ-102, RAZ-128, RAZ-104— · new (was localStorage)
attendance.publicListquerypublicpaged { eventId, limit ≤ 50 } → { attendees: [{ handle, name, city, photo?, verified }], counts: { going, verified, hidden } }RAZ-62, RAZ-86, RAZ-34, RAZ-115, RAZ-83, RAZ-304, RAZ-82plan.goingList, festivalSignup.publicRoster · change (one public rule)
attendance.countsquerypublic{ eventIds ≤ 100 } → Record<eventId, { going, verified, faces ≤ 10 }>RAZ-71, RAZ-86, RAZ-133plan.count, plan.countBatch, plan.goingBatch · change (merged)
attendance.proofShowcasequerypublic{} → Showcase or null (verified first)RAZ-77, RAZ-83plan.proofShowcase · change
attendance.claimTicketsmutationverified{} → { claimed, events }RAZ-83claim.claimMyTicketHolderRows · change (verified email required)
—claim.getClaimStatus · remove (tells anyone whether an email holds a ticket, ADR-003; the claim page asks the signed-in, verified user)
attendance.checkInmutationmember(owner, editor){ eventId, attendanceId } → { checkedInAt }map:meetup-1.7-activity-check-in, RAZ-96— · new

plans

TargetKindAuthInput → outputStoriesToday · status
plans.minequeryauthed{ year? } → Plan { attendances (event, ticket state, early bird), sessionPicks, enrolments, goals }RAZ-52, RAZ-70, RAZ-80, RAZ-88, RAZ-94, RAZ-123, RAZ-133, RAZ-140, RAZ-144, RAZ-147, RAZ-154, RAZ-38, RAZ-97, RAZ-136, RAZ-36, RAZ-91, RAZ-108, RAZ-113, map:schedule-compare-with-friendsplan.myPlan, plan.list, plan.listDetailed · change (merged, typed)
plans.sharedquerypublic{ handle } → SharedPlan (visible attendances only)RAZ-23, RAZ-48, RAZ-52, RAZ-154plan.sharedPlan · change
plans.pickSessionmutationauthed{ sessionId } → { ok }RAZ-97, RAZ-36, RAZ-54plan.add (workshop) · change
plans.unpickSessionmutationauthed{ sessionId } → { ok }RAZ-97, RAZ-36plan.remove (workshop) · change
plans.enrolmutationauthed{ seriesId } → { ok }RAZ-108, RAZ-113, RAZ-29plan.add (event + class metadata) · change
plans.unenrolmutationauthed{ seriesId } → { ok }RAZ-108plan.remove · change
plans.goals.upsertmutationauthed{ goalId?, kind, title, target, periodYear } → { goalId }RAZ-91plan.add (goal), plan.updateMetadata · change
plans.goals.deletemutationauthed{ goalId } → { ok }RAZ-91plan.remove (goal) · change
plans.discoverqueryauthed{} → DiscoverCard[] (visible attendances only)RAZ-127plan.discover · change (privacy fix, city_id)

social

TargetKindAuthInput → outputStoriesToday · status
social.waves.sendmutationauthed{ eventId, toDancerId } → { mutual }RAZ-67, RAZ-82, RAZ-102, RAZ-61, RAZ-65wave.send · change (event id; both must attend visibly)
social.waves.retractmutationauthed{ eventId, toDancerId } → { ok }RAZ-65wave.retract · change
social.waves.minequeryauthed{ eventId } → { sent, received, unanswered }RAZ-65, RAZ-82wave.myWaves, wave.wavesForMe, wave.incomingCount · change (merged)
social.waves.friendsqueryauthed{ eventId } → DancerCard[]RAZ-67, RAZ-82, RAZ-61, RAZ-65wave.friends · change
social.waves.poolqueryauthedpaged { eventId, partnerRole?, lookingForPartner? } → DancerCard[]RAZ-67, RAZ-82, RAZ-102, RAZ-61, RAZ-65, RAZ-124wave.discoverPool · change (lead/follow and "looking" filters)
social.rides.listquerypublic{ eventId } → Ride[] (public rule)RAZ-67, RAZ-116, map:meetup-1.2-activity-cardsrideShare.list · change
social.rides.upsertmutationauthed{ eventId, type, originCityId, departsOn, seats?, chatUrl? } → { ok }RAZ-67, RAZ-116, map:meetup-3.3-ride-group-chatrideShare.create · change
social.rides.deletemutationauthed{ eventId } → { ok }RAZ-116rideShare.delete · rename
social.rooms.listquerypublic{ eventId } → RoomRequest[] (public rule)RAZ-121, map:meetup-1.2-activity-cardsroommate.list · change
social.rooms.upsertmutationauthed{ eventId, checkIn?, checkOut?, budgetCents?, notes? } → { ok }RAZ-121roommate.toggle · change (no toggle)
social.rooms.deletemutationauthed{ eventId } → { ok }RAZ-121roommate.toggle · change
social.dinners.listquerypublic{ eventId } → Dinner[] (one query)RAZ-67, map:meetup-1.2-activity-cards, map:meetup-5.1-dinner-sign-up, map:meetup-5.3-dinner-chat-link, map:meetup-5.4-restaurant-revealdinner.list · change
social.dinners.joinmutationauthed{ dinnerId } → { joined }RAZ-67, map:meetup-5.1-dinner-sign-updinner.join · change (atomic capacity)
social.dinners.leavemutationauthed{ dinnerId } → { ok }map:meetup-5.1-dinner-sign-updinner.leave · rename
social.hangouts.listquerypublic{ cityId } → Hangout[] (derived counts)RAZ-118hangouts.listTonight · change
social.hangouts.createmutationauthed +RL{ kind, title, startsAt, venue?, cityId } → HangoutRAZ-118hangouts.create · change
social.hangouts.rsvpmutationauthed{ hangoutId } → { ok }RAZ-118hangouts.toggleRsvp · change
social.hangouts.unrsvpmutationauthed{ hangoutId } → { ok }RAZ-118hangouts.toggleRsvp · change
social.hangouts.closemutationauthed (owner){ hangoutId } → { ok }RAZ-118hangouts.close · rename

booking

TargetKindAuthInput → outputStoriesToday · status
booking.spaces.availabilityquerypublic{ profileId } → Slot[]RAZ-135, RAZ-158booking.availabilityForProfile · rename
booking.spaces.setAvailabilitymutationmember(owner, editor, moderator){ spaceId, slots } → { ok }RAZ-158booking.setAvailability · change (works: withTx, membership)
booking.requests.createmutationpublic +RL{ spaceId, email, name?, title?, kind?, styles?, artistNames?, organizerProfileId?, startsAt, endsAt?, headcount?, message?, ticketUrl?, termsAccepted } → { id }RAZ-129, RAZ-135, RAZ-142, RAZ-158, RAZ-159, RAZ-160booking.request · change (venue notified)
booking.requests.listForProfilequerymember(owner, editor, moderator)paged { profileId, status? } → BookingRequest[]RAZ-152, RAZ-166, RAZ-129, RAZ-158election.pendingBookings · change (also commercial venues)
booking.requests.decidemutationmember(owner, editor, moderator){ bookingId, action: accept or decline, note? } → { eventId? }RAZ-152, RAZ-166, RAZ-129, RAZ-158election.moderateBooking · change (accept creates the event)
booking.requests.minequeryauthedpaged {} → BookingRequest[]RAZ-135— · new
booking.requests.withdrawmutationauthed{ bookingId } → { ok }RAZ-142— · new

governance

TargetKindAuthInput → outputStoriesToday · status
governance.elections.forProfilequerypublic{ profileId } → Election + candidates + ledgerRAZ-152, RAZ-164, RAZ-165election.forProfile · rename
governance.elections.myVotequeryauthed{ electionId } → MyVoteRAZ-152, RAZ-165election.myVote · rename
governance.elections.openmutationmember(moderator) or admin{ profileId, termStart? } → { electionId }RAZ-152, RAZ-163election.open · change (membership auth)
governance.elections.nominatemutationverified{ electionId, guidelines, statement? } → { candidateId }RAZ-152, RAZ-163, RAZ-164election.nominate · change (verified email, not "has a username")
governance.elections.advancemutationmember(moderator) or admin{ electionId } → { ok }RAZ-152, RAZ-163election.advanceToVoting · rename
governance.elections.castVotemutationverified{ electionId, candidateId } → { changed }RAZ-152, RAZ-165election.castVote · change (atomic)
governance.elections.closemutationmember(moderator) or admin{ electionId } → { winnerCandidateId, votes }RAZ-152, RAZ-163, RAZ-165election.close · change (one transaction; grants moderator membership)
governance.guidelines.historyquerypublic{ profileId } → GuidelineVersion[]RAZ-166election.guidelineHistory · rename

community

TargetKindAuthInput → outputStoriesToday · status
community.reviews.listquerypublicpaged { eventId or profileId } → { items, average, count }map:meetup-8.1-rate-festival, RAZ-89, RAZ-157review.list · change (FK target)
community.reviews.upsertmutationauthed{ eventId or profileId, rating, text? } → { ok }map:meetup-8.1-rate-festival, RAZ-89review.create · change
community.askLocals.listquerypublic{ cityId } → Question[]RAZ-157, map:meetup-9.1-local-tipsaskLocals.listByCity · rename
community.askLocals.askmutationauthed +RL{ cityId, question } → { id }RAZ-157, map:meetup-9.1-local-tipsaskLocals.ask · rename
community.askLocals.recommendmutationauthed +RL{ requestId?, cityId, profileId or newPlace } → { profileId }RAZ-157, map:meetup-9.1-local-tipsaskLocals.recommend · change (stub profile, FK review)
community.videos.listquerypublic{ cityId, month? } → Video[]none (see below)cityVideo.listApproved · rename
community.videos.monthWinnerquerypublic{ cityId, month? } → Video or nullnonecityVideo.monthWinner · rename
community.videos.pairquerypublic{ cityId } → [Video, Video] or nullnonecityVideo.getPair · rename
community.videos.votemutationpublic +RL{ cityId, winnerId, loserId } → { recorded }nonecityVideo.vote · change (atomic ELO)
community.videos.submitmutationpublic +RL{ cityId, title, url, style?, email } → { id }nonecityVideo.submit · change
—cityVideo.battleMatchup, cityVideo.battleVote, cityVideo.cityLeaderboard · remove (see below)
—giveaway.listActive, giveaway.enter · remove unless confirmed (see below)

opportunities

TargetKindAuthInput → outputStoriesToday · status
opportunities.gigs.listquerypublicpaged { kind?, category?, cityId?, remote? } → GigCard[] (no contact email)RAZ-40, RAZ-41, RAZ-42, RAZ-43, RAZ-145, RAZ-148gigs.list · change
opportunities.gigs.getquerypublic{ id } → GigRAZ-41gigs.getById · rename
opportunities.gigs.createmutationauthed +RL{ kind, category, title, posterProfileId?, posterName, cityId?, remote, startsOn?, endsOn?, whenNote?, compensation, deadline?, contactEmail } → { id }RAZ-42, RAZ-43, RAZ-145, RAZ-148gigs.create · change
opportunities.gigs.closemutationauthed (owner){ id } → { ok }RAZ-42gigs.close · exists (wire the UI)
opportunities.gigs.applymutationpublic +RL{ gigId, message, email?, name? } → { id }RAZ-41— · new (stored + emailed to the poster)
opportunities.gigs.applicationsqueryauthed (owner){ gigId } → Application[]RAZ-42— · new

inbound

TargetKindAuthInput → outputStoriesToday · status
inbound.inquirymutationpublic +RL{ eventType?, date?, city?, guests?, needs, name?, email } → { id }RAZ-57, RAZ-63, RAZ-68, RAZ-73, RAZ-78, RAZ-81, RAZ-90, RAZ-96, RAZ-101, RAZ-107, RAZ-114, RAZ-138, RAZ-143feedback.inquiry · change (stored, outbox to Linear)
~~inbound.report~~———RAZ-272dropped: the "Talk to us" widget submits straight to Sentry User Feedback from the client (no tRPC procedure); feedback.reporter (protected query) only supplies the signed-in dancer's name + email

organizer

TargetKindAuthInput → outputStoriesToday · status
organizer.myEventsqueryauthed{} → events the caller manages via membershipsRAZ-79, RAZ-150festivalInsights.myFestivals · change
organizer.insights.styleLevelMixquerymember(owner, editor){ eventId } → MixRAZ-79, RAZ-150festivalInsights.styleLevelMix · change (attendances)
organizer.insights.workshopDemandquerymember(owner, editor){ eventId } → [{ sessionId, picks }]RAZ-74, RAZ-150— · new
organizer.insights.geographyquerymember(owner, editor){ eventId } → [{ cityId, count }]RAZ-85, RAZ-150— · new
organizer.insights.signalquerymember(owner, editor){ eventId } → reviews summary + waves madeRAZ-89, RAZ-150, map:meetup-8.2-festival-recap— · new

admin

The 13 existing admin procedures keep their behaviour and move into two sub-routers (names withheld here):

  • admin.dinners.* — the 7 procedures behind festival dinner management (festival list, dinner list and sign-ups, group assignment and listing, chat links, restaurant reveal). Stories: map:meetup-5.2-dinner-group-assignment, map:meetup-5.3-dinner-chat-link, map:meetup-5.4-restaurant-reveal, RAZ-170.
  • admin.content.* — the 6 procedures for video moderation, giveaways and community-group curation. Story: RAZ-170. The giveaway pair goes if the public giveaway procedures go.

New admin procedures: admin.festivals.onboard (submission → listing, RAZ-95, RAZ-112, RAZ-170), admin.claims.list / admin.claims.decide (ADR-004), admin.inbound.list / admin.inbound.update (ADR-010).

REST (target)

EndpointPurposeStatus
POST /api/webhooks/stripeStripe events → webhook_events ledger → orderschange (moved from /api/stripe/webhook)
POST /api/webhooks/tickettailorTicketTailor orders → orderschange (ledger, DB mapping)
POST /api/media/imageauthed image upload, 5 MB, image types onlychange (RAZ-288)
GET /api/cron/deliveries, /api/cron/series, /api/cron/cleanupoutbox, series materialisation, token/rate-limit purge (CRON_SECRET)new
GET /api/v1/public/events/:id/embed, GET /api/v1/public/profiles/:handle/events.icsorganiser widget, calendar feedsnew (map:meetup-1.8-organizer-embed-widget, RAZ-104)
GET /api/architecture/admingenerated admin docsexists
ANY /api/trpc/:trpctRPCexists
GET /api/cities, /api/cities/:slug, /api/festivals, /api/festivals/:slug, /api/city-*remove (SSR caller and directory.*)

Stories with no procedure

Need none — landing copy, client-only explainers or static links that are built: RAZ-25, RAZ-26, RAZ-27, RAZ-30, RAZ-31, RAZ-35, RAZ-37, RAZ-44, RAZ-46, RAZ-49, RAZ-51, RAZ-53, RAZ-56, RAZ-60, RAZ-72, RAZ-76, RAZ-87, RAZ-92, RAZ-93, RAZ-98, RAZ-103, RAZ-109, RAZ-111, RAZ-119, RAZ-120, RAZ-134, RAZ-137, RAZ-146. RAZ-153 is a one-off migration script; RAZ-247 is served by the Stripe webhook.

Served today by REST, get a procedure: RAZ-47, RAZ-139 → directory.cities.list; RAZ-168, RAZ-169 → directory.cities.get; RAZ-32, RAZ-105, RAZ-131 → festivals.list / festivals.get; RAZ-246 → ticketing.order.

Partial or missing, covered by the target: RAZ-75, RAZ-104, RAZ-128 → attendance.updateLogistics (+ iCal feed for RAZ-104); RAZ-287 → plans.enrol (payment later); RAZ-33 → ticketing.ticketTypes.upsert; RAZ-69, RAZ-149 → ticketing.ticketTypes.list; RAZ-85 → organizer.insights.geography; map:meetup-1.7-activity-check-in → attendance.checkIn; map:meetup-1.8-organizer-embed-widget → /api/v1/public/…/embed; map:meetup-3.3-ride-group-chat → social.rides.upsert.chatUrl; map:meetup-8.2-festival-recap → organizer.insights.signal; map:meetup-8.3-next-festival-invite → festivals.suggestions; map:meetup-1.4-plan-ready-message → an email from the deliveries outbox when a plan gains its first ticket (no procedure).

Not in the target — need a product decision first: RAZ-39, RAZ-100, RAZ-106 (organiser plans and billing); map:meetup-2.4-social-login (provider choice, ADR-003 option 2); map:meetup-1.9-live-feed; map:meetup-3.4-auto-ride-matching; map:meetup-6.4-taxi-dancer; map:meetup-7.1-extra-activities and map:meetup-7.2-propose-activity (community-proposed activities could be programme_sessions proposed by attendees); map:meetup-8.4-persistent-crew; map:meetup-9.4-ambassador.

Procedures with no story (the 16 unmapped)

ProcedureRecommendationWhy
auth.changePasswordkeep → identity.changePassword; add a storyused by /settings (app/composables/useAuth.ts)
cityVideo.listApprovedkeep → community.videos.list; write the storylive on /cities/[city] (app/pages/cities/[city]/index.vue); 23 videos in production
cityVideo.monthWinnerkeep → community.videos.monthWinnerVideoOfMonthCard.vue
cityVideo.getPairkeep → community.videos.pairCityVideoVote.vue
cityVideo.votekeep → community.videos.voteCityVideoVote.vue (ELO fix, ADR-002)
cityVideo.submitkeep → community.videos.submitSubmitVideoForm.vue
cityVideo.battleMatchupremoveCityFight.vue on /cities; 3 battle votes ever (city-canonicalisation.md:45); a second ranking system with no story
cityVideo.battleVoteremoveas above
cityVideo.cityLeaderboardremoveas above
giveaway.listActiveremove unless the Commander confirms a sponsor pipeline0 giveaways in production (city-canonicalisation.md:40), no story
giveaway.enterremove (same condition)as above
entity.listCitiesremove (merged into directory.cities.list)no UI caller; duplicates /api/cities
entity.cityDirectoryremove (merged into directory.cities.get)no UI caller; duplicates /api/cities/:slug
gigs.getByIdkeep → opportunities.gigs.getneeded for gig detail and apply (RAZ-41)
gigs.closekeep, wire the UIRAZ-42 notes: posters cannot close their gigs
referral.myReferralskeep → ticketing.referrals.mine (authed)shows referrer credit, part of RAZ-23

Conventions (from ADR-011)

  • Router per context; context.verb or context.resource.verb; no toggles.
  • Auth tier is a middleware, never an if inside a public procedure.
  • TRPCError only; SQLSTATE-based dbError(); zod issues returned as field errors.
  • Bounded inputs; explicit output DTOs; no PII in public DTOs.
  • Cursor pagination ({ cursor, limit ≤ 100 } → { items, nextCursor }).
  • Multi-statement writes in withTx; no N+1.
  • Checkout idempotency key; webhooks through webhook_events.
  • SSR through a server-side tRPC caller; REST only for webhooks, upload, cron and versioned public feeds (/api/v1/public/…).
  • tRPC is unversioned (expand/contract per procedure); public REST is versioned in the path.