Adding a category to a live directory is not one change. It is a schema change, a seeding job, an admin intake change, a routing change, and a sitemap change, and they cannot all ship in the same commit without a bad week.
The technique that makes this tractable is boring and worth writing down: put a single allowlist on the public read path and treat it as the feature switch for the entire rollout.
I work on the directory at Special Needs Care Network, which lists ABA therapy providers and special education schools across US cities. Speech therapy is the category currently going through this process, so the examples are from that.
The first stage already shipped: speech therapy is live as a requestable service in the intake form, captured as a distinct service on the inquiry record, and deliberately excluded from automatic routing until the provider vertical is live. Demand capture first, read path second, is the ordering the rest of this post argues for.
The allowlist
Every public read filters on one exported constant:
export const PUBLIC_DIRECTORY_V2_PROVIDER_TYPES = ['school', 'therapy_center'] as const;
Every query that serves the public site applies it:
function applyPublicProviderFilters(query) {
return query
.in('provider_type', [...PUBLIC_DIRECTORY_V2_PROVIDER_TYPES])
.eq('status', 'active')
.eq('visibility_status', 'published');
}
The database CHECK constraint can accept a new provider type. Rows of that type can exist. Admin tooling can edit them. None of it reaches a public page until the type is in that array.
That single property is what makes the rest safe.
What it buys
Schema and data work ship early and invisibly. Widening the type constraint, seeding the new type's rows, and backfilling locations are all no-ops from the public site's perspective. They can land days or weeks before launch, in separate reviewed deploys, each verifiable on its own.
Launch becomes one atomic deploy. Adding the type to the array, adding the route tree, and adding the URL branch go out together. There is no window where the type is half-public.
Rollback is a one-line revert. Remove the type, redeploy, and the pages disappear. Data stays. No migration is reversed, nothing is deleted, and nothing needs restoring.
The two failure modes worth guarding
The pattern has a specific hazard: the allowlist is not the only place that knows about types. Two others usually exist and both fail silently.
Hardcoded type lists in SQL views. Bundle and aggregate views tend to carry their own WHERE provider_type IN ('school','therapy_center'). These are invisible to the application-layer allowlist. Flip the switch without widening them and the new category's pages render, empty, with a 200. Empty pages that return 200 are worse than 404s: crawlers index them.
Fallback normalizers that collapse unknown values. A function mapping the new schema onto a legacy type token is a common transitional artifact:
function toLegacyListingType(providerType) {
return providerType === 'therapy_center' ? 'therapy' : 'school';
}
Every unknown type becomes 'school'. Add a third type without touching this and speech clinics render as schools, with school templates, school breadcrumbs, and school structured data. Nothing throws.
Both failures produce a 200 and a plausible-looking page. Neither is caught by type checking, because in both cases the types are still valid. Grep for the hardcoded list across SQL and application code before flipping anything, and make the normalizer exhaustive rather than defaulted.
The ordering rule
One invariant covers it:
Never add the type to the allowlist before every hardcoded list that also enumerates types has been widened and verified.
The verification worth doing is direct: insert one row of the new type, call the views and the resolver functions by hand, and confirm the row appears in each. Then confirm the live site still does not show it. That second check is the one that proves the switch is actually a switch.
The directory this comes from is at specialneedsusa.com, published by Special Needs Care Network.
Source: DEV Community