Podcast Slug Availability API
The live check behind the create-podcast Title and web address control (src/lib/components/podcast/PodcastAddressControl.svelte). The control's local tier (empty, bad characters, hyphens, length) never calls this; only a locally-valid slug is sent, debounced 420ms after the last keystroke.
Base Path: /api/podcasts/slug-availability
Authentication: Required (Bearer token). Only signed-in users create podcasts, and a slug oracle is worth hammering.
Rate limit: 60 requests per minute per user (scope api.podcasts.slug-availability, identifierMode: 'user_or_ip'), via the shared Postgres-backed limiter. Fails open on limiter errors.
Source: src/api/routes/podcasts/slug-availability.ts
Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
POST | / | Yes | Verdict for a candidate slug, plus alternatives |
Check a Slug
POST /api/podcasts/slug-availabilityRequest body:
{
"slug": "the-sarah-and-jo-show",
"title": "The Sarah and Jo Show"
}title is optional; it feeds the word-based alternative generator when the slug is taken.
Response (200):
{
"success": true,
"data": {
"verdict": "taken",
"alternatives": ["sarah-jo", "sarah-jo-show", "the-sarah-jo"]
}
}verdict is one of:
| Verdict | Meaning |
|---|---|
available | No podcast holds the slug and it is not guarded |
taken | A podcast already holds the slug; alternatives carries up to 3 free ones |
unavailable | The slug is a reserved label OR a blocked term (deliberately one verdict) |
A locally-invalid slug (empty, bad characters, leading or trailing hyphen, over 50 characters) returns 400 with the same message getSlugValidationError produces, and a malformed payload returns 400 from the validator.
Security Properties (D-5)
Two properties are load-bearing and tested (src/api/routes/podcasts/__tests__/slug-availability.test.ts):
- Reserved and blocked are indistinguishable. Both return the single
unavailableverdict with the same HTTP status and body shape, and no reason code. The blocked list lives insrc/lib/server/slug-guard/(server-only by directory) and must never reach a client bundle; this endpoint exists precisely so the browser never holds it. Anything that lets a caller tell the two apart turns the endpoint into a blocklist enumeration oracle. - Alternatives are generated AND filtered server-side. Candidates come from
suggestSlugAlternativesinsrc/lib/utils/slug.ts(title words with stop words stripped, then numeric fallbacks), then pass throughisGuardedSlugand one.in('slug', …)uniqueness query before any are returned. A guarded verdict returns no alternatives at all.
The Verdict Is Advisory
A green tick is not a reservation. The create action (src/routes/(app)/p/new/+page.server.ts) re-runs getSlugValidationError, isGuardedSlug and the global uniqueness query, and keeps its 23505 unique-violation race fallback. The action's slug failures carry a slugVerdict field (taken | unavailable | invalid) so the address control can land the server's answer without importing the guard.