Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commitf1ffd50unknown_key

perf(edge): ETag middleware + Cache-Control policy for Crontech edge layer

perf(edge): ETag middleware + Cache-Control policy for Crontech edge layer

Two pieces of caching infrastructure that lay the groundwork for
Crontech to act as Gluecron's edge layer (owner's call — keeping
the value capture in-house instead of using Cloudflare).

1. ETAG middleware on every non-streaming response. Hono's built-in
   hono/etag. Skipped on git protocol (.git/), live-events SSE,
   deploy event firehose, and /admin/status (all streaming). Repeat
   visits get 304 Not Modified — ~95% bandwidth saving when nothing
   changed.

2. CACHE-CONTROL middleware sets sensible defaults so any edge proxy
   in front (Crontech or otherwise) and downstream browsers know what
   to cache for how long:

   - /admin, /settings, /dashboard, /notifications, /connect/* and
     any request with a session cookie:  private, no-store
     (never cache responses tied to a session)

   - Marketing pages (/, /features, /pricing, /about, /vs-github,
     /explore, /help, /changelog, /legal/*, /terms, /privacy,
     /acceptable-use, /docs/*):
     public, max-age=60, s-maxage=300, stale-while-revalidate=86400
     (instant render from cache, 5 min at edge, 1 day SWR)

   - Public repo browse pages (GET /:owner/:repo and below):
     public, max-age=30, s-maxage=120, stale-while-revalidate=600
     (Crontech surge-purge on push hook is a follow-up)

   - Everything else: private, no-store (opt-in only).

   The ETag middleware sets its own 'private, no-cache, must-revalidate'
   default which we treat as 'no policy chosen' so our Cache-Control
   wins. Routes that set Cache-Control explicitly (package downloads,
   sitemaps, etc.) keep theirs.

Verified live: marketing pages get the public,SWR header; admin gets
private,no-store; 304 revalidation path still returns correctly. Full
test suite 2050/2050.
Claude committed on May 24, 2026Parent: 141561f
1 file changed+860f1ffd5088c6580bddf1c6399f7abf80182547401
1 changed file+86−0
Modifiedsrc/app.tsx+86−0View fileUnifiedSplit
22import { logger } from "hono/logger";
33import { cors } from "hono/cors";
44import { compress } from "hono/compress";
5import { etag } from "hono/etag";
56// BLOCK O2 — shared error-page surface (404 / 500 / 403).
67import { NotFoundPage, ServerErrorPage } from "./views/error-page";
78import { reportError } from "./lib/observability";
141142app.use("*", requestContext);
142143// Middleware — compression first (wraps all responses)
143144app.use("*", compress());
145
146// ETag middleware — returns 304 Not Modified on unchanged responses.
147// Saves ~95% bandwidth on repeat visits. Skipped on git protocol +
148// SSE + the API surface where it would interfere with streaming.
149app.use("*", async (c, next) => {
150 const p = c.req.path;
151 if (
152 p.includes(".git/") ||
153 p.startsWith("/live-events") ||
154 p.startsWith("/api/events/deploy") ||
155 p.startsWith("/admin/status")
156 ) {
157 return next();
158 }
159 return etag()(c, next);
160});
161
162// Cache-Control middleware — sets sensible defaults on public, anonymous
163// requests. Crontech (when wired as our edge layer) and downstream
164// browsers will honor these. Auth'd users get private,no-store
165// automatically — we never cache responses tied to a session.
166app.use("*", async (c, next) => {
167 await next();
168 // Don't overwrite explicit headers set by route handlers — BUT the
169 // ETag middleware unconditionally sets "private, no-cache, must-
170 // revalidate" so we have to treat that specific value as "no
171 // policy chosen yet" and apply ours. Any route that explicitly set
172 // a different cache-control wins.
173 const existing = c.res.headers.get("cache-control");
174 const etagDefault =
175 existing === "private, no-cache, must-revalidate" ||
176 existing === "no-cache";
177 if (existing && !etagDefault) return;
178 // Anything past auth: private + no-store (avoid leaking session
179 // content into shared caches). softAuth runs before this on the
180 // request, but the response side is what we're stamping.
181 const hasSession = c.req.header("cookie")?.includes("session=") ?? false;
182 const p = c.req.path;
183 // Always private for known-authed paths regardless of cookie.
184 if (
185 p.startsWith("/admin") ||
186 p.startsWith("/settings") ||
187 p.startsWith("/dashboard") ||
188 p.startsWith("/notifications") ||
189 p.startsWith("/connect/") ||
190 hasSession
191 ) {
192 c.res.headers.set("cache-control", "private, no-store");
193 return;
194 }
195 // Public marketing surfaces — short edge cache, longer browser cache,
196 // stale-while-revalidate so the user never waits on a stale fetch.
197 const isMarketing =
198 p === "/" ||
199 p === "/features" ||
200 p === "/pricing" ||
201 p === "/about" ||
202 p === "/vs-github" ||
203 p === "/explore" ||
204 p === "/help" ||
205 p === "/changelog" ||
206 p.startsWith("/legal/") ||
207 p === "/terms" ||
208 p === "/privacy" ||
209 p === "/acceptable-use" ||
210 p.startsWith("/docs/");
211 if (isMarketing) {
212 c.res.headers.set(
213 "cache-control",
214 "public, max-age=60, s-maxage=300, stale-while-revalidate=86400"
215 );
216 return;
217 }
218 // Public repo browse pages — cache aggressively. Edge invalidates
219 // on push via the post-receive hook (future Crontech surge purge).
220 if (/^\/[^/]+\/[^/]+(\/.*)?$/.test(p) && c.req.method === "GET") {
221 c.res.headers.set(
222 "cache-control",
223 "public, max-age=30, s-maxage=120, stale-while-revalidate=600"
224 );
225 return;
226 }
227 // Default: don't cache anything we haven't explicitly opted in.
228 c.res.headers.set("cache-control", "private, no-store");
229});
144230// Logger only on non-git routes to avoid overhead on clone/push
145231app.use("*", async (c, next) => {
146232 if (c.req.path.includes(".git/")) return next();
147233