@timber-js/app 0.2.0-alpha.154 → 0.2.0-alpha.156

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/agent-skill.md +344 -0
  2. package/dist/_chunks/{build-output-helper-B20ZuTSA.js → build-output-helper-Rp9r7gSA.js} +62 -9
  3. package/dist/_chunks/build-output-helper-Rp9r7gSA.js.map +1 -0
  4. package/dist/adapters/build-output-helper.d.ts +1 -0
  5. package/dist/adapters/build-output-helper.d.ts.map +1 -1
  6. package/dist/adapters/cloudflare.d.ts.map +1 -1
  7. package/dist/adapters/cloudflare.js +35 -5
  8. package/dist/adapters/cloudflare.js.map +1 -1
  9. package/dist/adapters/nitro.js +1 -1
  10. package/dist/adapters/shared.d.ts +1 -11
  11. package/dist/adapters/shared.d.ts.map +1 -1
  12. package/dist/cli.d.ts +9 -1
  13. package/dist/cli.d.ts.map +1 -1
  14. package/dist/cli.js +41 -4
  15. package/dist/cli.js.map +1 -1
  16. package/dist/fonts/google.d.ts.map +1 -1
  17. package/dist/index.js +43 -3
  18. package/dist/index.js.map +1 -1
  19. package/dist/plugins/fonts.d.ts.map +1 -1
  20. package/dist/server/asset-headers.d.ts +7 -3
  21. package/dist/server/asset-headers.d.ts.map +1 -1
  22. package/dist/server/internal.js +12 -6
  23. package/dist/server/internal.js.map +1 -1
  24. package/dist/server/pipeline-phases.d.ts.map +1 -1
  25. package/docs/api/30-api-server.mdx +233 -0
  26. package/docs/api/31-api-client.mdx +161 -0
  27. package/docs/api/32-api-cache.mdx +193 -0
  28. package/docs/api/33-api-search-params.mdx +141 -0
  29. package/docs/api/34-api-config.mdx +189 -0
  30. package/docs/api/35-api-typescript.mdx +170 -0
  31. package/docs/api/36-cli.mdx +143 -0
  32. package/docs/learn/00-introduction.mdx +81 -0
  33. package/docs/learn/01-quick-start.mdx +94 -0
  34. package/docs/learn/02-pages-and-layouts.mdx +138 -0
  35. package/docs/learn/03-fetching-data.mdx +211 -0
  36. package/docs/learn/03b-access-control.mdx +130 -0
  37. package/docs/learn/04-loading-states.mdx +67 -0
  38. package/docs/learn/04b-the-flush-point.mdx +115 -0
  39. package/docs/learn/05-typed-params.mdx +275 -0
  40. package/docs/learn/06-forms-and-actions.mdx +150 -0
  41. package/docs/learn/08-streaming.mdx +146 -0
  42. package/docs/learn/09-caching.mdx +121 -0
  43. package/docs/learn/10-middleware.mdx +156 -0
  44. package/docs/learn/11-error-handling.mdx +147 -0
  45. package/docs/learn/12-client-navigation.mdx +168 -0
  46. package/docs/learn/13-configuration.mdx +117 -0
  47. package/docs/learn/14-deploying.mdx +206 -0
  48. package/docs/more/01-advanced-routing.mdx +139 -0
  49. package/docs/more/02-advanced-forms.mdx +137 -0
  50. package/docs/more/03-coming-from-nextjs.mdx +186 -0
  51. package/docs/more/04-metadata-and-fonts.mdx +193 -0
  52. package/docs/more/04b-mdx.mdx +209 -0
  53. package/docs/more/05-content-collections.mdx +90 -0
  54. package/docs/more/06-instrumentation.mdx +188 -0
  55. package/docs/more/07-security.mdx +123 -0
  56. package/docs/more/40-why-timber.mdx +50 -0
  57. package/docs/more/41-timber-vs-nextjs.mdx +81 -0
  58. package/docs/more/42-timber-vs-others.mdx +68 -0
  59. package/docs/more/50-ai-agent-instructions.mdx +169 -0
  60. package/llms.txt +55 -0
  61. package/package.json +8 -4
  62. package/src/adapters/build-output-helper.ts +52 -2
  63. package/src/adapters/cloudflare.ts +34 -4
  64. package/src/adapters/shared.ts +34 -6
  65. package/src/cli.ts +62 -3
  66. package/src/fonts/google.ts +56 -5
  67. package/src/plugins/fonts.ts +6 -0
  68. package/src/server/asset-headers.ts +7 -3
  69. package/src/server/pipeline-phases.ts +10 -2
  70. package/dist/_chunks/build-output-helper-B20ZuTSA.js.map +0 -1
@@ -0,0 +1,81 @@
1
+ ---
2
+ title: 'Introduction'
3
+ description: 'Why timber.js exists'
4
+ slug: 'introduction'
5
+ notAI: true
6
+ ---
7
+
8
+ # Introduction
9
+
10
+ ## Basics
11
+
12
+ Timber is a react-based web framework built on vite and server components. It is inspired by next.js, tanstack start, and remix however it brings an independent approach built on a more modern foundation.
13
+
14
+ It is built on web standards and attempts to use as little framework magic as possible. We feel it unlocks both the most flexible UX and DX possible. Rather than using heuristics to adjust how the framework operates, it gives you the knobs to fine-tune your website to your liking.
15
+
16
+ That means it is entirely possible to write an inefficient website with timber, however, you will also _understand_ exactly what is happening and have clear eyes on how to fix it.
17
+
18
+ We believe that the best UX is often invisible, but the best DX is _transparent_. You shouldn't ever be confused about where something is running and why.
19
+
20
+ Timber is not a framework that comes with the simplest mental model – rather it embraces the _inherent complexity_ of the web and gives you the tools to define your boundaries as you see fit. Server and client, static and dynamic, build-time and run-time. So on and so forth. There is no one true _best_ place to put these boundaries, so it is incumbent on the developer to _choose_ where to place them.
21
+
22
+ Timber is a tool for frontend professionals. Though a beginner could pick it up and learn both the framework and the platform (the web) incrementally, it is best utilized by people who have a deep understanding of both the web and building websites.
23
+
24
+ Think of it as a flexible toolbelt of tools: the mental model to understand each of these tools and how to use them is high, but once you do, the framework clicks into place. A simpler mental model may come with it an easier skill curve, but at the sacrifice of both user experience and developer experience.
25
+
26
+ Timber is understandable, but you'll have to be patient in understanding it. We feel it's worth the investment.
27
+
28
+ ## Philosophy
29
+
30
+ All web frameworks are created out of some basic philosophies of how its creators feel websites should be built. Timber is no exception.
31
+
32
+ Here's ours:
33
+
34
+ A good web architecture in 2026 can scale up and down with the requirements of the project. That doesn't mean a simple target (e.g. a static blog) comes with it the simplest mental model, but that it can be functionally deployed in the most efficient manner. And that same architecture can scale up to a complex, dynamic website, both from a structural and a deployment perspective.
35
+
36
+ A good web architecture _embraces_ the complexity of building websites. Since building a website is inherently complex, a framework that embraces that complexity will feel complex and overwhelming at first. But it is easier to imagine it as a toolbelt that gives you access to each tool in the belt.
37
+
38
+ So while a beginner could pick up this framework, and it could incrementally teach them each component of building a powerful website, this framework is _not_ designed to be the simplest mental model. This is a framework for the frontend professional. Or a team building a complex and powerful web experience. People who want to understand:
39
+
40
+ * how the server and client co-exist (the network boundary)
41
+ * the distinction between build-time and request-time
42
+ * static routes and dynamic routes co-existing
43
+ * zero-javascript websites and fully static js-only SPAs
44
+ * how to build a composable architecture that still feels like it sits on web standards
45
+
46
+ Now this all sounds like a lot. I'm not here to say it isn't. But if you're a frontend professional, if you work on a team of experienced engineers, an architecture that sits across these complexities and doesn't abstract them gives you the power to define your boundaries: server and client, static and dynamic, client js and not.
47
+
48
+ You have to learn the inherent complexity of these ideas, but once you do, you internalize an architecture that can scale up and down. One that gives you access to both UX and DX that is unparalleled. Timber does not dumb down our abstractions to make it easier to _understand_, because that comes at the cost of optionality. It gives you the tools to _define_ your boundaries.
49
+
50
+ Other frameworks that have pursued this have either abstracted away too much, leaned on too much magic, or ignored web standards. Timber is _not_ smarter than you. It doesn't try to be. If you write an inefficient website in Timber, it won't stop you. But it also won't project onto you _how_ to build a website. It just gives you the foundation to control it yourself.
51
+
52
+ The good news is, you don't have to understand this all immediately. You can layer on your understanding one by one. And I promise, in the long-term it will click. But you may have to touch it yourself for it to click.
53
+
54
+
55
+ {/* Here are our basic philosophies:
56
+
57
+ - All websites sit somewhere across three environments. Embracing these three as first-party targets is a bid for developer autonomy, not confusion.
58
+ - Server
59
+ - Client
60
+ - Isomorphic (Universal SSR)
61
+ - Serverless is a flawed paradigm for rendering
62
+ - Though it can be useful at times, owning your _own_ server (and CPU cycles) will always be more performant and cost effective at anything beyond a basic scale
63
+ - An inefficiency in serverless means an unbounded/infinite bill, on a server it only means degraded performance
64
+ - A server means your database can live _next_ to your rendering layer, which will always outperform _holistic_ edge rendering in generic real-world scenarios
65
+ - Your framework should never be smarter than you
66
+ - Good UX is invisible, good DX is transparent
67
+ - It's better to be understandable than magic, which means performance is fine tunable – rather than automatic
68
+ - The web constantly serializes and deserializes strings, the framework should provide typing and parsing to those edges
69
+ - Search Params
70
+ - Cookies
71
+ - Segment URL Params
72
+ - and more...
73
+ - A good web-framework scales horizontally across your surface API (routes)
74
+ - Adding more routes or components shouldn't affect performance on others
75
+ - A good web framework has finely-tuned control over static (build-time) and dynamic (run-time) rendering
76
+ - A good web framework has the ability to scale up and down functionally and with developer clarity, which means deploying a website that:
77
+ - Can ship zero client javascript
78
+ - Can ship static content
79
+ - And can scale up to a completely dynamic website */}
80
+
81
+ More here soon...
@@ -0,0 +1,94 @@
1
+ ---
2
+ title: 'Quick Start'
3
+ description: 'Create a timber.js project and see it running in 60 seconds.'
4
+ slug: 'quick-start'
5
+ notAI: true
6
+ ---
7
+
8
+ # Quick Start
9
+
10
+ <Callout variant="tip" title="We recommend pnpm">
11
+ You can use any package manager, but we personally recommend `pnpm`.
12
+
13
+ You can install pnpm with `npm install -g pnpm`.
14
+ </Callout>
15
+
16
+ ## Create a Project
17
+
18
+
19
+ ```sh
20
+ pnpm create create-timber-app my-app
21
+ cd my-app && pnpm i
22
+ pnpm dev
23
+ ```
24
+
25
+ Open [http://localhost:3000](http://localhost:3000). You have a running app.
26
+
27
+ ## Project Structure
28
+
29
+ ```
30
+ my-app/
31
+ src/
32
+ app/
33
+ layout.tsx # Root layout — wraps every page
34
+ page.tsx # Home page (/)
35
+ globals.css
36
+ timber.config.ts # Framework configuration
37
+ vite.config.ts # Vite configuration
38
+ package.json
39
+ tsconfig.json
40
+ ```
41
+
42
+ The `app/` directory is your route tree. Files named `page.tsx` become pages. Files named `layout.tsx` coelesce and wrap the page. This composable structure scales very effectively.
43
+
44
+ ## Your First Page
45
+
46
+ Every page is a server component by default — it executes exclusively on the server and produces HTML.
47
+
48
+ ```tsx title="app/page.tsx"
49
+ export default function HomePage() {
50
+ return (
51
+ <div>
52
+ <h1>Hello, timber.js</h1>
53
+ <p>This page was server-rendered.</p>
54
+ </div>
55
+ );
56
+ }
57
+ ```
58
+
59
+ <Callout variant="tip" title="Server Components">
60
+ I know server components can seem intimidating, but I'd implore you to play with them. Even if you don't want to run a server, you can still use "server components" with distinct advantages.
61
+
62
+ You can build static SPAs or even zero-javascript websites with both server components and timber.
63
+ </Callout>
64
+
65
+ ## Adding a Second Route
66
+
67
+ Create `app/about/page.tsx`:
68
+
69
+ ```tsx title="app/about/page.tsx"
70
+ export default function AboutPage() {
71
+ return <h1>About</h1>;
72
+ }
73
+ ```
74
+
75
+ Navigate to `/about` and you'll see the About page.
76
+
77
+ ## Navigation
78
+
79
+ Links are added by importing the `Link` component.
80
+
81
+ ```tsx title="app/about/page.tsx"
82
+ import { Link } from '@timber-js/app/client';
83
+
84
+ export default function AboutPage() {
85
+ return (
86
+ <div>
87
+ <h1>About</h1>
88
+ <Link href="/">Go Home</Link>
89
+ </div>
90
+ );
91
+ }
92
+ ```
93
+
94
+ That's it. You now have a basic timber.js app up and running.
@@ -0,0 +1,138 @@
1
+ ---
2
+ title: 'Pages and Layouts'
3
+ description: 'File-system routing with page.tsx, layout.tsx, and nested directory structure.'
4
+ slug: 'pages-and-layouts'
5
+ notAI: true
6
+ ---
7
+
8
+ # Pages, Layouts, and Segments
9
+
10
+ timber.js uses file-based routing, inspired directly by next.js. Folders in the `app/` directory become routes. The directory structure maps to the URL structure, which maps to your component tree.
11
+
12
+ We tweaked a few things, but next's layouting and routing design is among its strongest points. This also makes it very easy to migrate to timber.
13
+
14
+ ## Pages
15
+
16
+ A `page.tsx` file makes a route:
17
+
18
+ ```
19
+ app/
20
+ page.tsx # /
21
+ about/
22
+ page.tsx # /about
23
+ blog/
24
+ page.tsx # /blog
25
+ ```
26
+
27
+ Each page exports a default component. Pages are server components by default — they can be async and fetch data directly.
28
+
29
+ ```tsx title="app/blog/page.tsx"
30
+ export default async function BlogPage() {
31
+ const data = await fetchContent();
32
+
33
+ return <h1>{data.title}</h1>;
34
+ }
35
+ ```
36
+
37
+ ## Segments
38
+
39
+ Each directory beneath the `app` directory is called a `segment`. In the example above, `about` and `blog` are segments.
40
+
41
+ A nested, dynamic segment would look something like: `./app/blog/[slug]/page.tsx`. In this case, `blog` and `[slug]` are the segments that make up this route pattern.
42
+
43
+ ## Layouts
44
+
45
+ All parental `layout.tsx` files wraps a page in its subtree. Layouts receive a `children` prop and persist across navigations — they don't re-render when you navigate between sibling pages.
46
+
47
+ ```tsx title="app/layout.tsx"
48
+ import type { ReactNode } from 'react';
49
+
50
+ export default function RootLayout({ children }: { children: ReactNode }) {
51
+ return (
52
+ <html lang="en">
53
+ <body>
54
+ <nav>My Site</nav>
55
+ <main>{children}</main>
56
+ </body>
57
+ </html>
58
+ );
59
+ }
60
+ ```
61
+
62
+ Layouts nest. Each directory level can have its own layout:
63
+
64
+ ```
65
+ app/
66
+ layout.tsx # Root layout — wraps everything
67
+ page.tsx # /
68
+ blog/
69
+ layout.tsx # Blog layout — wraps all blog pages
70
+ page.tsx # /blog
71
+ [slug]/
72
+ page.tsx # /blog/my-post
73
+ ```
74
+
75
+ When you visit `/blog/my-post`, the component tree looks like:
76
+
77
+ ```tsx
78
+ <RootLayout>
79
+ <BlogLayout>
80
+ <BlogPostPage />
81
+ </BlogLayout>
82
+ </RootLayout>
83
+ ```
84
+
85
+ The root layout shell (nav, footer) stays mounted when you navigate between blog posts. Only the innermost content re-renders.
86
+
87
+ ## Segment Convention Files
88
+
89
+ Each directory is called a **segment** and can contain these files:
90
+
91
+ | File | What it does |
92
+ | --------------- | --------------------------------------------------------- |
93
+ | `page.tsx` | The leaf content for this route |
94
+ | `layout.tsx` | Persistent UI shell — wraps children, survives navigation |
95
+ | `access.ts` | Authorization gate — runs before the layout renders |
96
+ | `middleware.ts` | Pre-render hook for headers, redirects, and cache warming |
97
+ | `params.ts` | Typed search params definitions (see [Typed Params](/docs/typed-params)) |
98
+ | `error.tsx` | Error boundary catching failures in this subtree |
99
+ | `404.tsx` | Denial page for 404 status code (must be a client component) |
100
+
101
+ None are required. A segment with just `layout.tsx` provides shared UI. One with just `page.tsx` is a leaf route.
102
+
103
+ <Callout variant="tip" title="No loading.tsx">
104
+ You'll notice there's no `loading.tsx` file. This is intentional and by design.
105
+
106
+ We'll learn more about _why_ later on.
107
+ </Callout>
108
+
109
+ ## Route Groups
110
+
111
+ Directories wrapped in parentheses create a segment boundary without affecting the URL:
112
+
113
+ ```
114
+ app/
115
+ (marketing)/
116
+ page.tsx # /
117
+ about/
118
+ page.tsx # /about
119
+ (dashboard)/
120
+ layout.tsx # Dashboard layout (shared sidebar, etc.)
121
+ settings/
122
+ page.tsx # /settings
123
+ ```
124
+
125
+ Use groups to organize by concern — auth boundaries, public vs private areas, or custom layout structures — without polluting URLs.
126
+
127
+ ## Private Folders
128
+
129
+ Prefix a directory with `_` to exclude it from routing:
130
+
131
+ ```
132
+ app/
133
+ _components/ # Not a route — co-locate shared code here
134
+ Button.tsx
135
+ page.tsx # /
136
+ ```
137
+
138
+ For dynamic segments, advanced routing patterns (parallel routes, intercepting routes, catch-all segments), and API routes, see [Advanced Routing](/docs/advanced-routing).
@@ -0,0 +1,211 @@
1
+ ---
2
+ title: 'Fetching Data'
3
+ description: 'Async server components, deny(404), and dynamic segments.'
4
+ slug: 'fetching-data'
5
+ notAI: true
6
+ ---
7
+
8
+ # Fetching Data
9
+
10
+ ```ts title="app/schema.ts" id="schema" hidden
11
+ import { defineSchema } from '@timber-js/app/params';
12
+ import { codec } from '@timber-js/app/codec';
13
+
14
+ export default defineSchema({
15
+ segmentParams: {
16
+ '[id]': codec.string,
17
+ },
18
+ });
19
+
20
+ declare function fetchProducts(opts: { take: number }): Promise<{ id: string; name: string }[]>;
21
+ ```
22
+
23
+ Timber and RSCs do not prescribe _where you must_ fetch data. They only give you _more optionality_ as to where.
24
+
25
+ **You** (the developer) can decide where and when you want to fetch data (whether it be the server, or the client, or at build-time). Let's walk through each.
26
+
27
+ Server components are async — in general, this is the first place to reach for data fetching (or querying).
28
+
29
+ ```tsx title="app/page.tsx"
30
+ export default async function HomePage() {
31
+ const products = await fetchProducts({ take: 10 });
32
+
33
+ return (
34
+ <ul>
35
+ {products.map((p) => (
36
+ <li key={p.id}>{p.name}</li>
37
+ ))}
38
+ </ul>
39
+ );
40
+ }
41
+ ```
42
+
43
+ Though it might feel confusing at first, you'll find that it's *often* the best place to fetch your data. It fits cleanly into the request/response model, prevents client-side waterfalls, and is generally the _fastest_ round-trip. Though there _will_ be scenarios where data fetching in other places (i.e. the client) make more sense depending on each context. Don't be afraid to mix and match data fetching, this is the whole point of timber: optionality.
44
+
45
+ ## Dynamic Segments
46
+
47
+ Wrap a directory name in brackets to create a dynamic segment that captures a URL parameter:
48
+
49
+ ```
50
+ app/
51
+ products/
52
+ [id]/
53
+ page.tsx # /products/42, /products/abc
54
+ ```
55
+
56
+ Access the param with `getSegmentParams()`. Import `SEGMENT_PATH` from `$segment` for exact types:
57
+
58
+ ```tsx title="app/products/[id]/page.tsx" use="schema"
59
+ import { SEGMENT_PATH } from './$segment';
60
+ import { deny, getSegmentParams } from '@timber-js/app/server';
61
+
62
+ declare function getProduct(id: string): Promise<{ name: string; description: string } | null>;
63
+
64
+ export default async function ProductPage() {
65
+ const { id } = getSegmentParams(SEGMENT_PATH);
66
+ const product = await getProduct(id);
67
+
68
+ if (!product) return deny(404);
69
+
70
+ return (
71
+ <div>
72
+ <h1>{product.name}</h1>
73
+ <p>{product.description}</p>
74
+ </div>
75
+ );
76
+ }
77
+ ```
78
+
79
+ `deny(404)` sends a real HTTP 404 — not a 200 with an error message. Search engines, CDNs, and `curl -f` all see the correct status code.
80
+
81
+ For typed params (numbers, UUIDs, enums), define codecs in `app/schema.ts`. See [Typed Params](/docs/typed-params).
82
+
83
+ ## Server Components Span Three Environments
84
+
85
+ React Server Components split your code into three worlds:
86
+
87
+ **Server components** (default) — run on the server only. Their code never reaches the client. You can `await` database queries, access secrets, import heavy libraries. Zero client-side JavaScript cost.
88
+
89
+ **Client components** (`'use client'`) — run in the browser (and on the server for the initial HTML render). Use them for `useState`, `useEffect`, event handlers, and browser APIs.
90
+
91
+ **Server actions** (`'use server'`) — functions that run on the server but are callable from the client. Used for mutations — form submissions, data updates.
92
+
93
+ These compose. A server component can render a client component as a child. Server actions wire the client back to the server for mutations:
94
+
95
+ ```tsx title="app/products/[id]/page.tsx" use="schema"
96
+ import { SEGMENT_PATH } from './$segment';
97
+ import { deny, getSegmentParams } from '@timber-js/app/server';
98
+ import { AddToCartButton } from './add-to-cart';
99
+
100
+ declare function getProduct(id: string): Promise<{ id: string; name: string } | null>;
101
+
102
+ export default async function ProductPage() {
103
+ const { id } = getSegmentParams(SEGMENT_PATH);
104
+ const product = await getProduct(id);
105
+ if (!product) return deny(404);
106
+
107
+ return (
108
+ <div>
109
+ <h1>{product.name}</h1>
110
+ <AddToCartButton productId={product.id} />
111
+ </div>
112
+ );
113
+ }
114
+ ```
115
+
116
+ ```tsx title="app/products/[id]/add-to-cart.tsx"
117
+ 'use client';
118
+
119
+ import { useState } from 'react';
120
+
121
+ export function AddToCartButton({ productId }: { productId: string }) {
122
+ const [added, setAdded] = useState(false);
123
+ return (
124
+ <button onClick={() => setAdded(true)}>
125
+ {added ? 'Added ✓' : 'Add to Cart'}
126
+ </button>
127
+ );
128
+ }
129
+ ```
130
+
131
+ The product page renders on the server (zero JS cost). The button hydrates on the client for interactivity.
132
+
133
+ ## No Implicit Caching
134
+
135
+ `fetch()` is never patched or cached. A bare `fetch()` call hits the network every time. If you want cross-request caching, use `timber.cache()` — see [Caching](/docs/caching). If you want single-request caching, use `React.cache`.
136
+
137
+ Timber will never try to optimize your code through _magic_. If you want to build an efficient website, you'll have to understand when and where to place your caching or optimizations.
138
+
139
+ ## Parallel Data Fetching
140
+
141
+ Fetch multiple things concurrently with `Promise.all`:
142
+
143
+ ```tsx title="app/dashboard/page.tsx"
144
+ declare function getUser(): Promise<{ name: string }>;
145
+ declare function getStats(): Promise<{ revenue: number }>;
146
+ declare function getNotifications(): Promise<{ id: string; text: string }[]>;
147
+
148
+ export default async function DashboardPage() {
149
+ const [user, stats, notifications] = await Promise.all([
150
+ getUser(),
151
+ getStats(),
152
+ getNotifications(),
153
+ ]);
154
+
155
+ return (
156
+ <div>
157
+ <UserHeader user={user} />
158
+ <StatsGrid stats={stats} />
159
+ <NotificationList items={notifications} />
160
+ </div>
161
+ );
162
+ }
163
+ ```
164
+
165
+ ## `React.cache` for Request Deduplication
166
+
167
+ If multiple components in the same request need the same data, use `React.cache`:
168
+
169
+ ```tsx title="lib/data.ts"
170
+ import { cache } from 'react';
171
+
172
+ declare function fetchUser(id: string): Promise<{ id: string; name: string; email: string }>;
173
+
174
+ export const getUser = cache(async (id: string) => {
175
+ return fetchUser(id);
176
+ });
177
+ ```
178
+
179
+ This deduplicates within a single request. For cross-request caching with TTL, use `timber.cache()` — see [Caching](/docs/caching).
180
+
181
+ ## Streaming Secondary Content
182
+
183
+ Primary content fetches outside `<Suspense>` — it can affect the status code. Secondary content goes inside `<Suspense>` — it streams after the shell flushes.
184
+
185
+ ```tsx title="app/products/[id]/page.tsx" use="schema"
186
+ import { Suspense } from 'react';
187
+ import { SEGMENT_PATH } from './$segment';
188
+ import { deny, getSegmentParams } from '@timber-js/app/server';
189
+
190
+ declare function getProduct(id: string): Promise<{ id: string; name: string } | null>;
191
+ declare function ProductHeader(props: { product: { name: string } }): React.ReactElement;
192
+ declare function ReviewsSkeleton(): React.ReactElement;
193
+ declare function ProductReviews(props: { productId: string }): React.ReactElement;
194
+
195
+ export default async function ProductPage() {
196
+ const { id } = getSegmentParams(SEGMENT_PATH);
197
+ const product = await getProduct(id);
198
+ if (!product) deny(404);
199
+
200
+ return (
201
+ <div>
202
+ <ProductHeader product={product} />
203
+ <Suspense fallback={<ReviewsSkeleton />}>
204
+ <ProductReviews productId={product.id} />
205
+ </Suspense>
206
+ </div>
207
+ );
208
+ }
209
+ ```
210
+
211
+ `getProduct` runs before the flush — if it fails, you get a real 404. Reviews stream in after. More on this in [The Flush Point](/docs/the-flush-point).
@@ -0,0 +1,130 @@
1
+ ---
2
+ title: 'Access Control: access.ts'
3
+ description: 'Per-segment access control with access.ts, deny(), and graceful slot degradation.'
4
+ slug: 'access-control'
5
+ notAI: true
6
+ ---
7
+
8
+ # Access Control
9
+
10
+ ```ts title="app/schema.ts" id="schema" hidden
11
+ import { defineSchema } from '@timber-js/app/params';
12
+ import { codec } from '@timber-js/app/codec';
13
+
14
+ import { getCookieJar, redirect } from '@timber-js/app/server';
15
+
16
+ declare function getSessionFromCookie(jar: ReturnType<typeof getCookieJar>): { userId: string } | null;
17
+ declare function getUser(userId: string): Promise<{ id: string; name: string }>;
18
+
19
+
20
+ export default defineSchema({
21
+ segmentParams: {
22
+ '[projectId]': codec.string,
23
+ },
24
+ });
25
+ ```
26
+
27
+ Most websites use access control of some kind. Instead of DRY-ing your access control or duplicating checks across your codebase, timber provides a _composable_ form of access control: `access.ts`.
28
+
29
+ If you place an `access.ts` file anywhere in your route tree, every sub-route will execute every `access.ts` in its parental tree.
30
+
31
+ You can `redirect`, `deny`, or otherwise gate access to an entire route group.
32
+
33
+ ## `access.ts`
34
+
35
+ ```ts title="app/(authenticated)/access.ts"
36
+ import { getCookieJar, redirect } from '@timber-js/app/server';
37
+
38
+ export default async function access() {
39
+ const session = getSessionFromCookie(getCookieJar());
40
+
41
+ if (!session) return redirect('/login');
42
+
43
+ const user = await getUser(); // getUser is wrapped in React.cache
44
+
45
+ if (!user.isVerified) return deny(401);
46
+ }
47
+ ```
48
+
49
+ `access.ts` is a gate — it passes or fails. Return values are discarded. If it doesn't throw or redirect, rendering proceeds.
50
+
51
+ `access` runs before a route's layout or page in sequence, so if it short-circuits, those components are never executed.
52
+
53
+ ## Sharing Data Between Access and Layout
54
+
55
+ Access checks and layouts share the same `React.cache` scope. Call the same cached function in both — the layout gets a free cache hit:
56
+
57
+ ```tsx title="app/(authenticated)/layout.tsx"
58
+ export default async function AuthLayout({ children }: { children: React.ReactNode }) {
59
+ const user = await getUser(); // Cache HIT — access.ts already resolved this
60
+
61
+ return (
62
+ <div>
63
+ <nav>{user.name}</nav>
64
+ <main>{children}</main>
65
+ </div>
66
+ );
67
+ }
68
+ ```
69
+
70
+ ## Nested Access Checks
71
+
72
+ Deeper segments add their own `access.ts` for additional authorization:
73
+
74
+ ```ts title="app/(authenticated)/projects/[projectId]/access.ts" use="schema"
75
+ import { SEGMENT_PATH } from './$segment';
76
+ import { deny, getSegmentParams } from '@timber-js/app/server';
77
+
78
+ declare function requireUser(): Promise<{ id: string; orgId: string }>;
79
+ declare function getProject(id: string): Promise<{ orgId: string } | null>;
80
+
81
+ export default async function access() {
82
+ const user = await requireUser(); // Cache HIT from parent
83
+ const { projectId } = getSegmentParams(SEGMENT_PATH);
84
+ const project = await getProject(projectId);
85
+ if (!project) deny(404);
86
+ if (project.orgId !== user.orgId) deny(); // Default 403
87
+ }
88
+ ```
89
+
90
+ Access runs root-to-leaf. If the root gate fails, deeper gates never execute.
91
+
92
+ ## `deny()`
93
+
94
+ `deny()` produces the correct HTTP status code and renders the matching status file:
95
+
96
+ ```ts
97
+ deny(); // 403 Forbidden (default)
98
+ deny(404); // 404 Not Found
99
+ deny(401); // 401 Unauthorized
100
+ ```
101
+
102
+ ## Slot Degradation
103
+
104
+ When `access.ts` denies a parallel route (slot), only that slot degrades — the page continues rendering:
105
+
106
+ ```
107
+ app/
108
+ dashboard/
109
+ layout.tsx # Receives { children, admin }
110
+ page.tsx
111
+ @admin/
112
+ access.ts # Checks admin role
113
+ page.tsx # Admin panel
114
+ denied.tsx # Shown when access fails
115
+ ```
116
+
117
+ If the user isn't an admin, `@admin` renders `denied.tsx` while the rest of the dashboard loads normally.
118
+
119
+ When access denies, that segment's `layout.tsx` never runs — `denied.tsx` renders in its place, wrapped only by layouts above the denying `access.ts`. This holds at every level of a slot's tree: a nested `@admin/reports/access.ts` denial prevents `@admin/reports/layout.tsx` from executing (and from fetching data it would serialize into the response).
120
+
121
+ ## Middleware vs Access
122
+
123
+ | | `middleware.ts` | `access.ts` |
124
+ | ------------- | -------------------------------------- | --------------------------------- |
125
+ | Runs | Before rendering | Inside React tree |
126
+ | Purpose | Lightweight checks, redirects, headers | Auth, data gating |
127
+ | `React.cache` | Not available | Active — shares scope with layout |
128
+ | Slots | N/A | Graceful degradation |
129
+
130
+ Use `middleware.ts` for headers, redirects, and cache warming. Use `access.ts` for auth — it has access to `React.cache` and supports slot degradation.