Features Config
Feature flags for every optional module — when to ship lean, when to layer back on, and how flags interact with API routes and crons.
Why this matters
Most directories ship 3–4 features they actually need on day one and 8–10 features they'll add later if growth justifies it. Trying to launch all features at once dilutes attention, multiplies bugs, and creates a UI that feels cluttered before you've even validated the niche. The features config is the cheapest way to keep your product surface honest — flip everything to false except the essentials, ship, and let user requests drive what comes back on.
The implementation matters too: a false flag doesn't just hide UI. It returns 404 from API routes, skips associated cron jobs, and removes the feature from the admin panel entirely. That means flagged-off features can't be hit by curl, can't fire scheduled work, and can't be accidentally re-enabled by a stale UI cache.
File: config/features.config.ts
Toggle features without touching code. When a flag is false:
- Related UI components hide themselves
- API routes return 404
- Cron jobs associated with the feature are skipped
The core platform (submissions, payments, auth, admin) is always active — you can't turn those off.
All flags
| Flag | Default | What it adds |
|---|---|---|
partners | true | Partner/sponsor slots in the sidebar (monthly subscriptions). |
badges | true | Embeddable "Featured on …" badges for winners. |
backlinks | true | Dofollow/nofollow toggle on outbound project links. |
socialProof | true | Live counters, "X people submitted today" widgets. |
newsletter | true | Email newsletter signup (uses Resend). |
blog | true | /blog section, SEObot integration for AI articles. |
webhooksExternal | true | Outbound webhooks (Discord/Slack) on events. |
adBanner | true | Top horizontal banner above the header. |
bookmarks | true | Logged-in users can save projects. |
ratings | true | 1–5 star ratings on project pages. |
comments | true | Comment threads on project pages. |
promotions | true | Paid ad placements (banner, catalog cards, detail cards). |
analytics | true | Enhanced tracking: device, browser, country, per-project events. |
map | true | Location fields, an embedded directory map, and a fullscreen globe. Needs NEXT_PUBLIC_MAPBOX_TOKEN to render. |
ai | false | AI-powered description generation, category suggestions & bulk CSV enrichment. |
i18n | false | Multi-language via next-intl. |
store | false | Store format (marketplace): product fields, cart, and native Stripe checkout. Only meaningful with vertical: 'store' — see Marketplace Config. |
jobs | false | Jobs format (job board): job-posting fields and an Apply CTA. Only meaningful with vertical: 'jobs'. |
localBusiness | true | Local business format: business fields + a Types facet. Only meaningful with vertical: 'localBusiness' — see Local Business Directory. |
claims | true | Business owners claim listings they didn't submit (email-domain verification + optional fee). |
quotes | true | Request-a-quote leads: RFQ from a listing or the global Get Quotes page, plus paid lead subscriptions. |
localSeo | false | Programmatic "service-in-city" landing pages and a /locations browser. |
Selecting vertical: 'localBusiness' in config/platform.config.ts auto-enables claims, quotes, localSeo and map and hides SaaS-only modules — you don't toggle each one by hand. Same idea as store/jobs: the vertical drives the format. See Local Business Directory.
map ships true, yet map surfaces only render once you set NEXT_PUBLIC_MAPBOX_TOKEN — until then they show a placeholder. Leave it on if you want geography later, or set map: false to drop it entirely. See Maps & Location.
Where flags are checked
In API routes
import { featureGuard } from '@/lib/features';
export async function GET() {
const guard = featureGuard('partners');
if (guard) return guard; // 404 response if disabled
// ... normal logic
}In server components
import { isFeatureEnabled } from '@/lib/features';
if (isFeatureEnabled('comments')) {
// render comments
}In client components
import { useFeatures } from '@/hooks/use-features';
function ProjectPage() {
const { isEnabled } = useFeatures();
return (
<>
<ProjectInfo />
{isEnabled('ratings') && <StarRating />}
{isEnabled('comments') && <Comments />}
</>
);
}Common recipes
"Free-only" directory (no payments)
Don't disable promotions — disable payments at the provider level instead. In config/payments.config.ts:
export const paymentsConfig = {
provider: 'none', // disables all paid flows
testMode: false,
};Launch MVP with minimum features
Hand the triage to AI — describe your niche and what matters, and let it set the flags:
To do it by hand, set everything except the essentials to false:
export const featuresConfig: FeaturesConfig = {
partners: false,
badges: false,
backlinks: true, // keep — nice for SEO
socialProof: false,
newsletter: true, // keep — grows your list
blog: false,
webhooksExternal: false,
adBanner: false,
bookmarks: false,
ratings: false,
comments: false,
promotions: false,
analytics: true, // keep — you need usage data
map: false, // unless your niche is location-based
ai: false,
i18n: false,
};Ship, watch what users ask for, turn flags back on one at a time.
Turning on AI features
- Set
ai: truehere - Set
AI_PROVIDER,AI_MODEL,AI_API_KEYin.env.local - See AI Config for options
A real-world walkthrough: a 14-day "minimum viable directory" feature plan
Day-one launch with the smallest sustainable surface:
- Submissions, payments, auth, admin — always on (core platform).
backlinks: true— every project has a do-follow outbound link by default. Flip to nofollow only after you grow enough that link-juice farming becomes a problem.newsletter: true— email capture is the only retention loop that survives without re-acquisition costs.analytics: true— you cannot iterate without seeing what works.- Everything else:
false.
Week two, layer back features in order of validation:
- If submissions exceed 30/week, enable
socialProofto show momentum. - If users start asking "what's new?", enable
blogand ship 3 pillar posts. - If a project owner asks "can I be at the top?", enable
promotionsand price at $39–69/month. - If sponsorship inquiries arrive, enable
partners. Don't enable proactively. - Enable
ratingsonly when you have 1,000+ MAU; below that, the signal is noise. - Enable
commentsonly when you have moderation bandwidth. - Enable
aiif your niche generates enough listings that hand-written descriptions become the bottleneck. - Enable
i18nonly when you have a real translation, not before.
This sequence keeps the directory feeling polished at every stage — features ship when they have content to be useful, not because the codebase supports them.
Common pitfalls
- Flipping
commentsandratingson a fresh directory. Empty comment sections and "(no ratings yet)" widgets make the site look abandoned. Enable both only after you have user volume. - Disabling
analyticsto "save data". The built-in tracker is GDPR-friendly by design (no PII). Disabling it means you launch blind. Keep it on. - Forgetting to clear API caches after flipping a flag. The UI hides immediately, but cached API responses (Next.js fetch cache, edge cache) might serve old data for an hour. Use
revalidatePathor wait for natural expiry. - Editing the config from the admin panel. There's no admin-panel toggle by design — the file is committed to git so flag changes are reviewable. Editing through git keeps the audit trail.
- Hardcoding
isFeatureEnabledchecks. Use the helper functions (featureGuard,isFeatureEnabled,useFeatures) — they centralise the lookup and let you migrate to env-driven flags later without rewriting every check.
FAQ
Can I have different features enabled per environment?
Yes — read flag values from env vars in the config file. Pattern: ai: process.env.AI_FEATURE_ENABLED === 'true'. Lets you ship AI to production while keeping it off in staging, for example. Not all flags benefit from this; the heavyweights (ai, i18n, analytics) are the most common candidates.
What happens to existing data when I flag a feature off?
Nothing — the data stays in the DB. Flagging comments: false hides the UI and 404s the API, but the comments table is untouched. Re-enabling restores everything. Only run a migration if you intend to permanently remove the feature.
Do flagged-off features affect bundle size?
Server components: yes (Next.js tree-shakes flagged-off code paths). Client components: usually no — the import graph still pulls in disabled feature components. If bundle size matters, dynamically import heavy client features and gate the dynamic import on the flag.