@@ -14,7 +14,7 @@ adapter layer for a production deployment.
1414| Security response headers (CSP/XCTO/…) | ** On** | ` @objectstack/runtime ` |
1515| HSTS | Off (opt-in) | ` securityHeaders.hsts: true ` |
1616| Token-bucket rate limit | Off (opt-in) | ` RateLimiter ` primitive |
17- | CSRF | Adapter-layer concern | helmet / @ fastify /csrf-protection |
17+ | CSRF | Adapter-layer concern | ` hono/secure-headers ` / ` hono /csrf` |
1818| Auth (better-auth) | On | ` @objectstack/plugin-auth ` |
1919| Project membership (RBAC) | On when scoped | dispatcher plugin |
2020| Field- and row-level perms | On | SecurityPlugin |
@@ -71,37 +71,40 @@ const write = new RateLimiter(DEFAULT_RATE_LIMITS.write); // 60 req / min / IP
7171const read = new RateLimiter (DEFAULT_RATE_LIMITS .read ); // 600 req / min / IP
7272```
7373
74- ### Fastify recipe
74+ ### Hono recipe
7575
7676``` ts
77- import Fastify from ' fastify' ;
78- import { objectStackPlugin } from ' @objectstack/fastify' ;
77+ import { Hono } from ' hono' ;
78+ import { secureHeaders } from ' hono/secure-headers' ;
79+ import { objectStackMiddleware } from ' @objectstack/hono' ;
7980import { RateLimiter , DEFAULT_RATE_LIMITS } from ' @objectstack/runtime' ;
80- import helmet from ' @fastify/helmet' ;
8181
82- const app = Fastify ({ trustProxy: 1 }); // trust ONE upstream proxy hop
83- await app .register ( helmet , { contentSecurityPolicy: false });
82+ const app = new Hono ();
83+ app .use ( ' * ' , secureHeaders ()); // CSP / X-Content-Type-Options / X-Frame-Options / …
8484
8585const buckets = {
8686 auth: new RateLimiter (DEFAULT_RATE_LIMITS .auth ),
8787 write: new RateLimiter (DEFAULT_RATE_LIMITS .write ),
8888 read: new RateLimiter (DEFAULT_RATE_LIMITS .read ),
8989};
9090
91- app .addHook (' onRequest' , async (req , reply ) => {
92- const ip = req .ip ;
93- const bucket = req .url .startsWith (' /api/v1/auth/' ) ? ' auth'
94- : [' POST' ,' PUT' ,' PATCH' ,' DELETE' ].includes (req .method ) ? ' write'
91+ // Rate-limit BEFORE the dispatcher middleware so rejected requests never reach it.
92+ app .use (' /api/v1/*' , async (c , next ) => {
93+ const ip = c .req .header (' x-forwarded-for' )?.split (' ,' )[0 ]?.trim () ?? ' unknown' ;
94+ const bucket = c .req .path .startsWith (' /api/v1/auth/' ) ? ' auth'
95+ : [' POST' ,' PUT' ,' PATCH' ,' DELETE' ].includes (c .req .method ) ? ' write'
9596 : ' read' ;
9697 const decision = buckets [bucket ].consume (` ${ip }:${bucket } ` );
97- reply .header (' X-RateLimit-Remaining' , String (decision .remaining ));
98+ c .header (' X-RateLimit-Remaining' , String (decision .remaining ));
9899 if (! decision .allowed ) {
99- reply .header (' Retry-After' , String (Math .ceil (decision .retryAfterMs / 1000 )));
100- return reply . code ( 429 ). send ( { error: ' Too many requests' });
100+ c .header (' Retry-After' , String (Math .ceil (decision .retryAfterMs / 1000 )));
101+ return c . json ( { error: ' Too many requests' }, 429 );
101102 }
103+ await next ();
102104});
103105
104- app .register (objectStackPlugin , { kernel , prefix: ' /api/v1' });
106+ app .use (' /api/v1/*' , objectStackMiddleware (kernel )); // dispatcher last
107+ export default app ; // Cloudflare Workers / Bun / Deno / Node (@hono/node-server)
105108```
106109
107110For multi-instance deploys, replace the default ` MemoryStore ` with a
@@ -131,8 +134,8 @@ Bearer …`. With bearer tokens stored in `localStorage` / memory there
131134is no CSRF surface — browsers don't auto-attach the header.
132135
133136CSRF protection becomes required when you switch to ** cookie-based
134- session auth** . In that case wire ` @fastify /csrf-protection ` (or the
135- equivalent for your adapter) and exempt only the auth callback routes.
137+ session auth** . In that case wire a CSRF middleware (e.g. ` hono /csrf` )
138+ and exempt only the auth callback routes.
136139
137140## JWT / session lifecycle
138141
@@ -167,13 +170,18 @@ curl -H "Authorization: Bearer $TOK" $API/data/account
167170
168171CORS is intentionally ** not** opinionated by the runtime — it's an
169172app-level policy that depends on which origins host your front-end.
170- Configure at the adapter:
173+ Configure it on the Hono adapter:
171174
172175``` ts
173- import cors from ' @fastify/cors' ;
174- await app .register (cors , {
175- origin: [' https://app.example.com' ],
176- credentials: true ,
176+ import { createHonoApp } from ' @objectstack/hono' ;
177+
178+ const app = createHonoApp ({
179+ kernel ,
180+ prefix: ' /api/v1' ,
181+ cors: {
182+ origin: [' https://app.example.com' ],
183+ credentials: true ,
184+ },
177185});
178186```
179187
0 commit comments