CTX Directory is a modern Next.js application for discovering, publishing, bookmarking, and remixing AI prompts and rules. It features categories and subcategories, creator profiles, privacy-aware visibility controls (public/private), and analytics.
This repository contains the web app. Data is powered by Supabase (Postgres, Auth, Storage), and usage analytics are optionally sourced from PostHog.
- Next.js 15 (App Router) with React 19 and TypeScript
- Tailwind CSS v4 and Radix UI primitives
- TanStack Query for client data fetching and caching
- Supabase (Auth, Postgres, Storage) via
@supabase/ssr
and@supabase/supabase-js
- PostHog client analytics (
posthog-js
) and server-side HogQL queries (optional)
Key configuration files:
next.config.ts
— stripsconsole.*
in productionsrc/app/layout.tsx
— global metadata and SEO defaultssrc/instrumentation-client.ts
— safe client-side PostHog initialization
Prerequisites:
- Node.js 18+ (Node 20+ recommended)
- pnpm (preferred) or npm/yarn
- A Supabase project (URL + keys)
- Install dependencies
pnpm install
# or: npm install / yarn install
- Create
.env.local
At minimum you need your Supabase environment and a public app URL for SEO/canonical metadata.
# Supabase (required)
NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
# Server-only (optional but recommended for profile avatar uploads)
SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key
# App base URL used for metadata, sitemap, and server calls
NEXT_PUBLIC_APP_URL=http://localhost:3000
# PostHog (optional)
NEXT_PUBLIC_POSTHOG_KEY=phc_xxx # client key (optional)
NEXT_PUBLIC_POSTHOG_HOST=https://us.posthog.com
# Server-side PostHog for analytics APIs (optional)
POSTHOG_PERSONAL_API_KEY=phx_xxx
POSTHOG_PROJECT_ID=12345
POSTHOG_API_HOST=https://us.posthog.com
Notes:
- Never expose
SUPABASE_SERVICE_ROLE_KEY
to the browser. It is read server-side in API routes only. - When deployed on Vercel,
VERCEL_URL
is auto-populated; some server utilities fall back to it when building absolute URLs.
- Run the dev server
pnpm dev
# or: npm run dev / yarn dev / bun dev
Visit http://localhost:3000
See package.json
.
{
"scripts": {
"dev": "next dev --turbopack",
"build": "next build --turbopack",
"start": "next start",
"lint": "eslint"
}
}
src/
app/ Next.js App Router (routes, API routes, layouts)
api/ Server routes (REST-ish JSON)
...
components/ UI components (screens, cards, forms, navigation)
content/ SEO/copy helpers
contexts/ React contexts (e.g., auth, theme)
lib/ Shared libraries
api/ Client-side wrappers for API calls
server/ Server-only helpers (e.g., rate limiting)
supabase/ Supabase client/server factories
types/ Centralized shared TS types
public/ Static assets (icons, og.png, manifest)
Notable files and utilities:
src/lib/server/rate-limit.ts
— in-memory fixed-window rate limiter that emitsX-RateLimit-*
headers.src/lib/validation/prompts.ts
— shared prompt validation and limits.src/lib/types/*
— canonical TypeScript types for prompts, creators, categories.
Client (public):
NEXT_PUBLIC_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY
NEXT_PUBLIC_APP_URL
(e.g.,https://ctx.directory
, used for metadata and server-side fetches)NEXT_PUBLIC_POSTHOG_KEY
(optional)NEXT_PUBLIC_POSTHOG_HOST
(optional)
Server-only:
SUPABASE_SERVICE_ROLE_KEY
— used insrc/app/api/profile/avatar/route.ts
for uploads that must bypass RLS.POSTHOG_PERSONAL_API_KEY
— required for analytics endpoints that query HogQL.POSTHOG_PROJECT_ID
POSTHOG_API_HOST
(optional, defaults to PostHog US)
- The app assumes a Supabase schema with tables including
prompts
,profiles
,prompt_bookmarks
,categories
, andsubcategories
. RLS is configured so public content is readable by everyone and private content is only readable by its owner. - Avatar uploads go to the
profile-avatars
storage bucket via the API routesrc/app/api/profile/avatar/route.ts
. WhenSUPABASE_SERVICE_ROLE_KEY
is configured, uploads bypass storage RLS while still requiring an authenticated user.
Visibility semantics are consistent across server and client:
public
— visible to everyoneprivate
— visible only to the authorall
— server-side logic allows authors to see their own private items in otherwise public lists
Key API routes were updated to default to visibility=all
where appropriate so creators see their private prompts in their own views.
All endpoints are implemented under src/app/api/
. Requests and responses are JSON. Many endpoints support caching headers and/or include rate limit headers.
-
GET /api/prompts
- Query params:
visibility
(all
|public
|private
, defaultall
),type
(default
|rule
),categoryId
,subcategoryId
,authorId
,search
,sort
(recommended
|newest
|bookmarked
),limit
(max 100),cursor
,ids
(CSV) - Returns
{ items, nextCursor, hasMore }
with keyset pagination
- Query params:
-
POST /api/prompts
- Body:
{ title, description?, content, tags?: string[], categoryId?, subcategoryId?, visibility?: 'public'|'private', type?: 'default'|'rule', parentId? }
- Rate limit: 10/hour per user
- Body:
-
GET /api/prompts/[id]
andGET /api/prompts/slug/[slug]
- Respect visibility; private prompts are only returned to their author
-
PUT /api/prompts/[id]
- Update prompt fields; regenerates slug when title changes
- Rate limit: 60/hour per user
-
DELETE /api/prompts/[id]
- Deletes a prompt (and related bookmarks)
- Rate limit: 10/hour per user
-
POST /api/prompts/[id]/bookmark
andDELETE /api/prompts/[id]/bookmark
- Toggle bookmarks; cannot bookmark own prompt
- Rate limit: 60/min per user
-
GET /api/prompts/bookmarks
- Returns
{ promptIds }
for the authenticated user
- Returns
-
GET /api/prompts/popular
andGET /api/prompts/newest
- Query params:
visibility
,type
,limit
- Query params:
-
GET /api/prompts/author/[authorId]
- Query params:
visibility
(defaultall
),type
,limit
- Query params:
-
GET /api/prompts/analytics
- Auth required; returns aggregate stats for the authenticated author (optionally accepts
authorId
when equal to current user) - Returns totals and top prompts for the creator dashboard
- Auth required; returns aggregate stats for the authenticated author (optionally accepts
-
GET /api/categories
- Query param:
type
(prompt
|rule
|both
), returns{ categories, subcategories }
- Query param:
-
GET /api/creators
- Query params:
limit
,offset
,includeUserId
- Query params:
-
GET /api/creators/[id]/metrics
- Computes usage metrics; uses PostHog (optional) if
POSTHOG_*
server envs are set - Rate limit: 60/min per IP
- Computes usage metrics; uses PostHog (optional) if
-
GET /api/usernames/check/[username]
- Validates availability and format
- Rate limit: 30/min per IP
-
POST /api/profiles
- Create/update your profile; validates username and URLs
-
GET /api/profiles/[id]
- Returns public profile info
-
POST /api/profile/avatar
- Auth required; uploads to
profile-avatars
bucket - Uses service role key when available to bypass RLS for storage operations
- Auth required; uploads to
-
POST /api/feedback
- Body:
{ topic: 'bug'|'feature'|'other', message: string, path?: string, userAgent?: string }
- Rate limit: 30/hour per user
- Body:
-
GET /api/analytics/views
- Query params:
type
(default
|rule
),slugs
(CSV) - Requires
POSTHOG_PERSONAL_API_KEY
+POSTHOG_PROJECT_ID
server envs - Rate limit: 60/min per IP
- Query params:
Rate limiting is implemented via src/lib/server/rate-limit.ts
and returns X-RateLimit-*
headers on applicable responses.
Shared in src/lib/validation/prompts.ts
:
- Title: 3–50 chars
- Content: 10–5000 chars
- Description: up to 300 chars
- Max 5 tags
src/app/layout.tsx
sets global metadata, Open Graph/Twitter defaults, icons, and manifest.src/app/sitemap.ts
andsrc/app/robots.ts
are configured viaNEXT_PUBLIC_APP_URL
.- Client analytics are initialized in
src/instrumentation-client.ts
(safe on the client only). - Server analytics endpoints require PostHog server credentials.
Recommended: Hetzner VPS with Coolify.
-
Set Environment Variables in your hosting platform (Coolify):
NEXT_PUBLIC_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY
SUPABASE_SERVICE_ROLE_KEY
(server-only)NEXT_PUBLIC_APP_URL
(your public domain; e.g.,https://ctx.directory
)- Optional PostHog variables if you use analytics APIs
-
Build and run
pnpm build && pnpm start
- If avatar uploads fail with a 401 or 403, ensure the request is authenticated and
SUPABASE_SERVICE_ROLE_KEY
is set server-side. Seesrc/app/api/profile/avatar/route.ts
. - If analytics endpoints return 500, check
POSTHOG_PERSONAL_API_KEY
andPOSTHOG_PROJECT_ID
. - If categories/creators appear empty, verify your Supabase schema and RLS policies for public reads.
- For SEO issues (wrong canonical URLs), ensure
NEXT_PUBLIC_APP_URL
is set correctly without a trailing slash.
Licensed under the MIT License. See the LICENSE
file for details.
"CTX", "CTX Directory", and related brand assets are trademarks of their respective owner(s). Use of these brand assets is not covered by the MIT license and requires prior permission. Forks and derivatives must not imply endorsement by or affiliation with the CTX Directory project.