@@ -380,6 +380,35 @@ export interface CronHandle {
380380 readonly isPaused : boolean
381381}
382382
383+ /**
384+ * HTTP route handler context.
385+ * Provides access to store and update functions for custom HTTP endpoints.
386+ */
387+ export interface HttpHandlerContext < S extends object > {
388+ readonly req : Request
389+ readonly url : URL
390+ readonly getStore : ( ) => S
391+ readonly update : ( fn : ( s : S ) => S ) => void
392+ }
393+
394+ /**
395+ * HTTP route handler function.
396+ * Returns a Response or void (void returns 200 OK with empty body).
397+ */
398+ export type HttpHandler < S extends object > = (
399+ ctx : HttpHandlerContext < S >
400+ ) => Response | Promise < Response > | void | Promise < void >
401+
402+ /**
403+ * HTTP route configuration.
404+ */
405+ export interface HttpConfig < S extends object > {
406+ /** HTTP method(s) to match. Defaults to ["GET", "POST"] */
407+ readonly method ?: string | string [ ]
408+ /** Route handler */
409+ readonly handler : HttpHandler < S >
410+ }
411+
383412// ============================================================================
384413// App Configuration (passed to .app())
385414// ============================================================================
@@ -486,6 +515,26 @@ export interface HyperstarFactory<
486515 handler : ( ctx : UserTriggerContext < S , U > , change : UserTriggerChange < T > ) => void
487516 } ) : void
488517
518+ /**
519+ * Define a custom HTTP endpoint.
520+ * Useful for webhooks, cron triggers, health checks, APIs.
521+ *
522+ * @example
523+ * ```tsx
524+ * app.http("/cron", async (ctx) => {
525+ * await runPoll(ctx)
526+ * return new Response(JSON.stringify({ ok: true }))
527+ * })
528+ *
529+ * // With specific method
530+ * app.http("/api/data", { method: "GET", handler: (ctx) => {
531+ * return Response.json(ctx.getStore())
532+ * }})
533+ * ```
534+ */
535+ http ( path : string , handler : HttpHandler < S > ) : void
536+ http ( path : string , config : HttpConfig < S > ) : void
537+
489538 /**
490539 * Configure the app with store, view, and other options.
491540 * Returns a servable app.
@@ -560,6 +609,7 @@ export const createHyperstar = <
560609 const cronDefs : Array < { id : string ; config : CronConfig < S , U > } > = [ ]
561610 const triggerDefs : Array < { id : string ; config : Omit < TriggerConfig < S , unknown > , "id" > } > = [ ]
562611 const userTriggerDefs : Array < { id : string ; config : Omit < UserTriggerConfig < S , U , unknown > , "id" > } > = [ ]
612+ const httpDefs : Array < { path : string ; methods : string [ ] ; handler : HttpHandler < S > } > = [ ]
563613
564614 // Create signal handles lazily via Proxy - handles are created on first access
565615 // Uses universal handles with all methods; TypeScript restricts visibility based on Signals type
@@ -629,6 +679,16 @@ export const createHyperstar = <
629679 userTriggerDefs . push ( { id, config : config as UserTriggerConfig < S , U , unknown > } )
630680 } ,
631681
682+ http ( path : string , handlerOrConfig : HttpHandler < S > | HttpConfig < S > ) : void {
683+ const isConfig = typeof handlerOrConfig === "object" && "handler" in handlerOrConfig
684+ const handler = isConfig ? handlerOrConfig . handler : handlerOrConfig
685+ const methodInput = isConfig ? handlerOrConfig . method : undefined
686+ const methods = methodInput
687+ ? ( Array . isArray ( methodInput ) ? methodInput : [ methodInput ] ) . map ( m => m . toUpperCase ( ) )
688+ : [ "GET" , "POST" ]
689+ httpDefs . push ( { path, methods, handler } )
690+ } ,
691+
632692 app ( config : HyperstarConfig < S , U , Signals > ) : HyperstarApp {
633693 // Signal defaults from config
634694 const signalDefaults : Record < string , unknown > = config . signals ?? { }
@@ -1035,6 +1095,34 @@ export const createHyperstar = <
10351095 }
10361096 }
10371097
1098+ // Check custom HTTP routes
1099+ for ( const { path : routePath , methods, handler } of httpDefs ) {
1100+ if ( reqPath === routePath && methods . includes ( req . method ) ) {
1101+ try {
1102+ const ctx : HttpHandlerContext < S > = {
1103+ req,
1104+ url,
1105+ getStore,
1106+ update : updateStore ,
1107+ }
1108+ const result = await handler ( ctx )
1109+ if ( result instanceof Response ) {
1110+ return result
1111+ }
1112+ // void return = 200 OK
1113+ return new Response ( JSON . stringify ( { ok : true } ) , {
1114+ headers : { "Content-Type" : "application/json" } ,
1115+ } )
1116+ } catch ( error ) {
1117+ console . error ( `[hyperstar] HTTP handler error for ${ routePath } :` , error )
1118+ return new Response ( JSON . stringify ( { error : String ( error ) } ) , {
1119+ status : 500 ,
1120+ headers : { "Content-Type" : "application/json" } ,
1121+ } )
1122+ }
1123+ }
1124+ }
1125+
10381126 return new Response ( "Not Found" , { status : 404 } )
10391127 }
10401128
0 commit comments