Skip to content

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

MethodPathAuthDescription
POST/YesVerdict for a candidate slug, plus alternatives

Check a Slug

POST /api/podcasts/slug-availability

Request body:

json
{
	"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):

json
{
	"success": true,
	"data": {
		"verdict": "taken",
		"alternatives": ["sarah-jo", "sarah-jo-show", "the-sarah-jo"]
	}
}

verdict is one of:

VerdictMeaning
availableNo podcast holds the slug and it is not guarded
takenA podcast already holds the slug; alternatives carries up to 3 free ones
unavailableThe 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 unavailable verdict with the same HTTP status and body shape, and no reason code. The blocked list lives in src/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 suggestSlugAlternatives in src/lib/utils/slug.ts (title words with stop words stripped, then numeric fallbacks), then pass through isGuardedSlug and 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.

Internal documentation - Not for public distribution