Skip to content

Commit b0898a2

Browse files
committed
docs
1 parent 075d2d3 commit b0898a2

13 files changed

Lines changed: 1018 additions & 23 deletions

docs/caching.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Caching Strategy
2+
3+
This project uses Next.js [Cache Components](https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents) (`cacheComponents: true` in `next.config.ts`) for granular server-side caching. This enables the `"use cache"` directive, `cacheLife()`, `cacheTag()`, and Partial Prerendering (PPR).
4+
5+
## How `"use cache"` works
6+
7+
Functions and components marked with `"use cache"` have their results cached across requests. The cache key is derived from the function's arguments and source location. Two APIs control cache behavior:
8+
9+
- **`cacheLife(profile)`** — sets the TTL. Accepts named presets (`"seconds"`, `"minutes"`, `"hours"`, `"days"`, `"weeks"`, `"max"`) or custom objects.
10+
- **`cacheTag(tag)`** — associates the cache entry with a tag for on-demand invalidation via `revalidateTag(tag)`.
11+
12+
See the [Next.js `"use cache"` reference](https://nextjs.org/docs/app/api-reference/directives/use-cache) for full documentation.
13+
14+
## Cache tag registry
15+
16+
All cache tag strings are defined as top-level functions in `src/server/manifest/cache-tags.ts`:
17+
18+
```ts
19+
docsFileSearchTag({ repo, branch, slug }) // per-file doc entries
20+
docsTreeTag({ repo, branch }) // per-branch file tree
21+
docsBranchesTag({ repo }) // per-repo branch list
22+
docsSearchIndexTag({ repo }) // per-repo search index
23+
```
24+
25+
Both `cacheTag()` (in `"use cache"` functions) and `revalidateTag()` (in the webhook handler) import from this module. A typo or rename becomes a compile-time error.
26+
27+
## Four-layer docs cache
28+
29+
Documentation content is cached at four levels of granularity. Each layer is a `"use cache"` function in `src/server/manifest/docs-cache.ts`:
30+
31+
### Layer 1: Per-file doc entries
32+
33+
`getDocFileEntries(repo, branch, slug)` — caches one markdown file's parsed title, description, headings (with depth), breadcrumbs, and URL. Consumers: search index, page TOC.
34+
35+
### Layer 2: Per-branch doc tree
36+
37+
`getDocTreeCached(repo, branch)` — caches the file tree structure (which files exist, folder hierarchy, meta.json ordering). Used by the sidebar to build the page tree.
38+
39+
### Layer 3: Per-repo branch list
40+
41+
`getDocBranchesCached(repo)` — caches the list of branches and which is the default.
42+
43+
### Layer 4: Per-repo search index (composite)
44+
45+
`getDocsSearchIndex(repo)` — calls Layers 2 and 1 to assemble a flat array of `DocPageEntry[]` for the search API. Since inner calls are independently cached, only invalidated pieces recompute when this layer rebuilds.
46+
47+
## Cache invalidation
48+
49+
A GitHub webhook (`POST /api/github/docs-webhook`) fires on every push. The handler in `src/server/docs/revalidate.ts` determines which tags to invalidate based on which files changed:
50+
51+
- **Modified file** → invalidates that file's search tag + the repo's search index
52+
- **Added/removed file** → also invalidates the branch tree tag
53+
- **meta.json changed** → invalidates the branch tree tag
54+
55+
The `searchIndex` tag is always invalidated alongside file-level changes so the composite index rebuilds. Unaffected files' entries stay cached.
56+
57+
## Cached pages
58+
59+
### Doc pages (`/docs/[...path]`)
60+
61+
Each doc page uses `"use cache"` at the page level, caching the entire rendered RSC payload. Tag-based invalidation on push ensures pages update immediately when content changes.
62+
63+
### Static pages
64+
65+
Pages with no dynamic data use `"use cache"` to enable static prerendering:
66+
67+
- `/` (homepage), `/community`, `/events`, `/partners`, `/legal/privacy`
68+
69+
These are effectively static — they rebuild only on deploy or when the cache expires.
70+
71+
## What is NOT cached
72+
73+
User-specific data (profile, permissions, verification, roles) runs fresh per-request. These queries live inside `<Suspense>` boundaries so they don't block the static shell from rendering.
74+
75+
The search API route (`/api/search`) fetches user permissions fresh per-request for filtering, but reads doc entries from the cached search index.
76+
77+
[Vercel Edge Config](https://vercel.com/docs/edge-config) is used for the documentation repo list — reads are ~1ms from CDN.
78+
79+
## PPR interaction
80+
81+
With PPR, the static shell of a page is prerendered and served from CDN. Dynamic holes (wrapped in `<Suspense>`) stream in as they resolve. For the navigation, this means the sidebar chrome renders instantly while user data streams in.
82+
83+
See [Navigation System](navigation) for the full PPR architecture.

docs/contributing.md

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
---
2-
title: Contributing
3-
description: Guidelines for contributing to the DevDogs Website.
4-
---
5-
6-
# Contributing
1+
# Contributing!!
72

83
Thanks for helping improve the DevDogs Website!
94

@@ -29,6 +24,23 @@ pnpm db:push
2924

3025
This pulls the introspected schema, pushes migrations, and regenerates Supabase types.
3126

27+
### Remote Supabase
28+
29+
If you've set up `.env.remote` (see
30+
[Getting Started](./getting-started.md#remote-supabase-optional)), use:
31+
32+
```bash
33+
pnpm db:push:remote
34+
```
35+
36+
instead of `pnpm db:push` to push schema changes and regenerate types
37+
against the **remote** database.
38+
39+
> **Warning:** This applies schema changes directly to whatever database
40+
> `.env.remote`'s `DB_URL` points to. Double-check `.env.remote` before
41+
> running this — never point it at a production database. `drizzle-kit
42+
> push` shows a diff and prompts for confirmation before applying changes.
43+
3244
## Documentation
3345

3446
Add or update markdown files in `docs/` alongside your code changes. Use the local preview to check rendering:

docs/database.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Database & Migrations
2+
3+
## Philosophy
4+
5+
SQL migration files are the source of truth. The Drizzle TypeScript schema (`src/server/db/schema/generated/`) is generated from the live database and never edited by hand. The only hand-maintained schema file is `src/server/db/relations.ts`, which defines Drizzle relational query structure on top of the generated types.
6+
7+
This means:
8+
- **To change the schema**, write SQL — not TypeScript.
9+
- **The TypeScript types follow** from the SQL, not the other way around.
10+
- **RLS policies, triggers, functions, and storage policies** all live in migration files alongside table DDL, with no workarounds needed.
11+
12+
Drizzle is used exclusively as the type-safe query layer. It does not own the schema.
13+
14+
## Making a schema change
15+
16+
```
17+
pnpm db:migration:new <name>
18+
```
19+
20+
This creates `supabase/migrations/<timestamp>_<name>.sql`. Write your DDL in that file.
21+
22+
```sql
23+
-- Example: add a column
24+
alter table "public"."profiles" add column "website" text;
25+
```
26+
27+
Then apply it locally and regenerate TypeScript types:
28+
29+
```
30+
pnpm db:migrate
31+
```
32+
33+
Test locally, then verify the migration replays correctly from scratch:
34+
35+
```
36+
pnpm db:reset
37+
```
38+
39+
If you added new tables or foreign keys, update `src/server/db/relations.ts` to add the corresponding Drizzle relations. Commit the migration file and any relations changes together.
40+
41+
## Injecting extra SQL
42+
43+
Anything that goes beyond plain table DDL belongs directly in the migration file — RLS policies, triggers, functions, storage policies, seed data for system rows:
44+
45+
```sql
46+
-- Adding a table with RLS
47+
create table "public"."announcements" (
48+
"id" uuid not null default gen_random_uuid(),
49+
"body" text not null,
50+
"createdAt" timestamp without time zone not null default now()
51+
);
52+
53+
alter table "public"."announcements" enable row level security;
54+
55+
create policy "announcements_read"
56+
on "public"."announcements"
57+
as permissive for select
58+
to authenticated
59+
using (true);
60+
```
61+
62+
There is no Drizzle workaround layer — write SQL and it works.
63+
64+
## Scripts reference
65+
66+
| Script | What it does |
67+
|---|---|
68+
| `pnpm db:migration:new <name>` | Create a new empty migration file in `supabase/migrations/` |
69+
| `pnpm db:migrate` | Apply pending migrations → regenerate TS types → seed built-in roles |
70+
| `pnpm db:reset` | Wipe local DB, replay all migrations from scratch → regenerate TS types → seed roles |
71+
| `pnpm db:pull` | Regenerate TS types from the current DB state without applying migrations |
72+
| `pnpm db:seed-roles` | Seed the built-in Member and Root roles (idempotent) |
73+
| `pnpm db:push:remote` | Push pending migrations to the linked production Supabase project |
74+
| `pnpm sb:start` | Start local Supabase, export credentials to `.env.local`, seed storage buckets |
75+
| `pnpm sb:stop` | Stop local Supabase |
76+
| `pnpm sb:restart` | Stop and restart local Supabase |
77+
| `pnpm dev` | Full local dev startup: start Supabase → apply migrations → start Next.js |
78+
79+
## Multi-contributor workflow
80+
81+
Database migrations have ordering constraints that code changes don't. Follow these rules to avoid conflicts:
82+
83+
**Generate migration files late.** Don't run `pnpm db:migration:new` at the start of a feature branch. Iterate locally using `pnpm db:migrate` as you figure out the schema, then generate the migration file when the feature is ready to merge — after rebasing onto `main`.
84+
85+
**One migration per PR.** A PR should produce at most one migration file covering all schema changes for that feature. This keeps the history readable and reduces the surface area for conflicts.
86+
87+
**Rebase before generating.** Before running `pnpm db:migration:new`:
88+
```
89+
git fetch && git rebase origin/main
90+
pnpm db:reset # re-apply all existing migrations on a clean slate
91+
```
92+
Then apply your schema changes on top and generate the file. The migration will be generated against the latest baseline rather than a stale one.
93+
94+
**CI verifies with `pnpm db:reset`.** Every PR should run `pnpm db:reset` in CI to confirm all migrations replay cleanly in order. This catches conflicts before merge, not after.
95+
96+
If two contributors generate migrations from the same baseline that touch the same tables, one of them must manually reconcile after merge. `pnpm db:reset` will surface the conflict immediately.
97+
98+
## Production deployment
99+
100+
Migrations are applied to the linked Supabase project with:
101+
102+
```
103+
pnpm db:push:remote
104+
```
105+
106+
This runs `supabase db push`, which applies only the migrations that haven't yet been applied to the remote project (tracked by Supabase's internal migration history table).
107+
108+
**Never run `drizzle-kit push` against a production database.** That command pushes directly without a migration record and has no rollback path.
109+
110+
## Drizzle config files
111+
112+
| File | Purpose |
113+
|---|---|
114+
| `drizzle.config.ts` | Public schema — used for `drizzle-kit pull` (generates `src/server/db/schema/generated/`) |
115+
| `drizzle-introspection.config.ts` | Non-public schemas (`auth`, `storage`, etc.) — generates `src/supabase/drizzle/schema.ts` |
116+
117+
Both configs point at the local DB URL and are only used with `drizzle-kit pull`. Neither is used for migrations.
118+
119+
## For sibling projects
120+
121+
The same workflow applies to other DevDogs-UGA projects that use Supabase:
122+
123+
1. Manage migrations in `supabase/migrations/` via the Supabase CLI (`supabase migration new`, `supabase migration up`, `supabase db reset`).
124+
2. Use `drizzle-kit pull` to generate TypeScript types from the DB after applying migrations.
125+
3. Maintain a manual `relations.ts` for Drizzle relational queries.
126+
4. Never use `drizzle-kit push` against production.
127+
128+
For Flutter projects: use the Supabase CLI for migration management; the Drizzle layer is not applicable.
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Documentation System Architecture
2+
3+
This page describes the technical design of the DevDogs documentation system — how it fetches and caches content, how pages are rendered, how the preview tool works, and how changes propagate from GitHub to the live site.
4+
5+
## Rendering pipeline
6+
7+
All documentation pages use a single shared component: `DocPageContent` (`src/components/DocPageContent/`). It accepts raw markdown source and pre-computed headings, then renders using [Fumadocs UI](https://fumadocs.vercel.app) with the following plugins:
8+
9+
| Plugin | Purpose |
10+
| --- | --- |
11+
| `remark-gfm` | GitHub Flavored Markdown (tables, task lists, strikethrough) |
12+
| `remark-heading` | Heading ID generation for anchor links |
13+
| `remark-admonition` | `> [!NOTE]` / `> [!WARNING]` / `> [!TIP]` callouts |
14+
| `remark-code-tab` | Code tab blocks |
15+
| `rehype-code` | Shiki syntax highlighting with dual light/dark themes |
16+
17+
`DocPageContent` is a pure renderer — it does not parse headings. Heading data (id, title, depth) comes from the cached manifest, ensuring the table of contents, search index, and sidebar tree all derive from the same source.
18+
19+
## No front matter
20+
21+
Markdown files are pure content. There is no YAML front matter. The page title is always the first `# ` heading; sidebar ordering and labels come from `meta.json` files placed alongside the markdown files.
22+
23+
## URL structure
24+
25+
Documentation URLs follow the pattern `/docs/{repo}/{branch}/{slug}`:
26+
27+
```
28+
/docs/devdogs-website/main/getting-started
29+
/docs/devdogs-website/main/contributing
30+
```
31+
32+
Only files under the `docs/` directory of a repository are served. Branch names can contain slashes — the routing resolves the branch via longest-prefix matching against known branch names.
33+
34+
## Content delivery and caching
35+
36+
The docs system uses Next.js [Cache Components](https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents) (`"use cache"` directive) for granular, per-file caching. Four independently cached layers compose at read time:
37+
38+
| Layer | Function | Cache tag | TTL |
39+
| --- | --- | --- | --- |
40+
| Per-file entries | `getDocFileEntries()` | `docs-search-{repo}-{branch}-{slug}` | `cacheLife("days")` |
41+
| Per-branch tree | `getDocTreeCached()` | `docs-tree-{repo}-{branch}` | `cacheLife("days")` |
42+
| Per-repo branches | `getDocBranchesCached()` | `docs-branches-{repo}` | `cacheLife("days")` |
43+
| Per-repo search index | `getDocsSearchIndex()` | `docs-search-index-{repo}` | `cacheLife("days")` |
44+
45+
All cache tag strings are defined in `src/server/manifest/cache-tags.ts` and shared between `cacheTag()` and `revalidateTag()` call sites — a typo becomes a compile-time error rather than a silent cache miss.
46+
47+
The per-file layer caches each markdown file's parsed headings, title, description, and breadcrumbs. When only one file changes, only that file's cache entry is invalidated — all other files remain cached.
48+
49+
Doc pages themselves use `"use cache"` at the page level, caching the entire rendered RSC payload. See the [caching overview](../caching) for the full picture.
50+
51+
**References:**
52+
- [Next.js `"use cache"` directive](https://nextjs.org/docs/app/api-reference/directives/use-cache)
53+
- [Next.js `cacheTag`](https://nextjs.org/docs/app/api-reference/functions/cacheTag)
54+
- [Next.js `cacheLife`](https://nextjs.org/docs/app/api-reference/functions/cacheLife)
55+
56+
## GitHub webhook (ISR)
57+
58+
When a push is made to a repository, GitHub sends a webhook to `POST /api/github/docs-webhook`. The handler (`src/server/docs/revalidate.ts`):
59+
60+
1. Verifies the `X-Hub-Signature-256` HMAC signature using `GITHUB_WEBHOOK_SECRET`.
61+
2. Parses the push event to extract the repo name, branch, and changed file paths.
62+
3. Calls `revalidateTag()` for the affected cache tags.
63+
64+
| Event | Tags invalidated |
65+
| --- | --- |
66+
| Any push | `docsBranchesTag`, `docsSearchIndexTag` |
67+
| File added/removed under `docs/` | `docsTreeTag`, `docsSearchIndexTag` |
68+
| File modified under `docs/` | `docsFileSearchTag`, `docsSearchIndexTag` |
69+
| `meta.json` changed | `docsTreeTag`, `docsSearchIndexTag` |
70+
71+
### Registering the webhook
72+
73+
In GitHub → Organization Settings → Webhooks:
74+
75+
- **Payload URL**: `https://devdogsuga.uga.edu/api/github/docs-webhook`
76+
- **Content type**: `application/json`
77+
- **Secret**: value of `GITHUB_WEBHOOK_SECRET` in your environment
78+
- **Events**: **Push** (covers branch pushes and PR merges)
79+
80+
### Environment variable
81+
82+
```
83+
GITHUB_WEBHOOK_SECRET="<random string, at least 20 characters>"
84+
```
85+
86+
Generate a secret with `openssl rand -hex 32`.
87+
88+
## Code organization
89+
90+
Documentation server code is consolidated in `src/server/docs/`:
91+
92+
| File | Responsibility |
93+
| --- | --- |
94+
| `github.ts` | Raw GitHub API calls (branches, tree, file content) |
95+
| `tree.ts` | Tree processing: meta.json parsing, ordering |
96+
| `utils.ts` | Shared string utilities (`toTitleCase`, `stripExt`) |
97+
| `actions.ts` | Server actions for sidebar (branch list, tree nodes, branch resolution) |
98+
| `revalidate.ts` | Webhook-based cache invalidation |
99+
100+
The `"use cache"` functions live in `src/server/manifest/docs-cache.ts` and compose the functions above.
101+
102+
## Sidebar metadata (`meta.json`)
103+
104+
`buildDocTree()` in `src/server/docs/tree.ts` processes the git tree. From it:
105+
106+
1. Collects all markdown files under `docs/`
107+
2. Fetches `docs/meta.json` and `docs/**/meta.json`
108+
3. Applies ordering, label overrides, and visibility filtering
109+
4. Returns `{ entries, metaByFolder }`
110+
111+
The sidebar component receives `DocTreeNode[]` — a data-source-agnostic tree type shared between the remote docs sidebar and the local preview sidebar.
112+
113+
## Local preview
114+
115+
The preview server (`@devdogsuga/docs-preview`) is a lightweight Node.js HTTP + WebSocket server that:
116+
117+
- Returns a structured `DocTreeNode[]` from `GET /tree` (no client-side tree building needed)
118+
- Serves raw file content from `GET /file?path=...`
119+
- Broadcasts `{ type: "change" }` events over WebSocket when any `docs/` file changes
120+
121+
The preview pages (`/tools/docs`) call `fetchFile()` from `@devdogsuga/docs-preview/client`. A client component (`PreviewRefreshClient`) connects the WebSocket and calls `router.refresh()` on each change event.

0 commit comments

Comments
 (0)