11import { z } from "zod" ;
22import { getRunMetadata } from "@/actor/config" ;
33import type {
4- BaseActorDefinition ,
54 AnyActorDefinition ,
5+ BaseActorDefinition ,
66} from "@/actor/definition" ;
77import {
88 KEYS ,
9- queueMetadataKey ,
109 queueMessagesPrefix ,
10+ queueMetadataKey ,
1111 workflowStoragePrefix ,
1212} from "@/actor/keys" ;
1313import { ENGINE_ENDPOINT } from "@/common/engine" ;
1414import { type Logger , LogLevelSchema } from "@/common/log" ;
15- import { DeepReadonly , VERSION } from "@/utils" ;
15+ import { VERSION } from "@/utils" ;
1616import { tryParseEndpoint } from "@/utils/endpoint-parser" ;
1717import {
18+ getNodeEnv ,
1819 getRivetEndpoint ,
1920 getRivetEngine ,
2021 getRivetNamespace ,
@@ -37,7 +38,9 @@ export type RegistryActors = z.infer<typeof ActorsSchema>;
3738export const TestConfigSchema = z . object ( { enabled : z . boolean ( ) } ) ;
3839export type TestConfig = z . infer < typeof TestConfigSchema > ;
3940
40- // TODO: Add sane defaults for NODE_ENV=development
41+ const RuntimeModeSchema = z . enum ( [ "envoy" , "serverless" ] ) ;
42+ export type RuntimeMode = z . infer < typeof RuntimeModeSchema > ;
43+
4144export const RegistryConfigSchema = z
4245 . object ( {
4346 // MARK: Actors
@@ -144,12 +147,18 @@ export const RegistryConfigSchema = z
144147 httpHost : z . string ( ) . optional ( ) ,
145148
146149 // MARK: Engine
150+ /**
151+ * @experimental
152+ *
153+ * Runtime mode to use when `registry.start()` is called.
154+ */
155+ mode : RuntimeModeSchema . optional ( ) ,
147156 /**
148157 * @experimental
149158 *
150159 * Starts the full Rust engine process locally.
151160 */
152- startEngine : z . boolean ( ) . default ( ( ) => getRivetRunEngine ( ) ) ,
161+ startEngine : z . boolean ( ) . optional ( ) ,
153162 /** @experimental */
154163 engineVersion : z
155164 . string ( )
@@ -193,7 +202,12 @@ export const RegistryConfigSchema = z
193202 *
194203 * Must be >= rivetkit-core's drain timeout (20s) + margin.
195204 */
196- gracePeriodMs : z . number ( ) . int ( ) . min ( 1_000 ) . optional ( ) . default ( 30_000 ) ,
205+ gracePeriodMs : z
206+ . number ( )
207+ . int ( )
208+ . min ( 1_000 )
209+ . optional ( )
210+ . default ( 30_000 ) ,
197211 /**
198212 * If true, rivetkit will not install SIGINT/SIGTERM handlers.
199213 * Use when the host application owns signal policy and will
@@ -202,10 +216,19 @@ export const RegistryConfigSchema = z
202216 disableSignalHandlers : z . boolean ( ) . optional ( ) . default ( false ) ,
203217 } )
204218 . optional ( )
205- . default ( ( ) => ( { gracePeriodMs : 30_000 , disableSignalHandlers : false } ) ) ,
219+ . default ( ( ) => ( {
220+ gracePeriodMs : 30_000 ,
221+ disableSignalHandlers : false ,
222+ } ) ) ,
206223 } )
207224 . transform ( ( config , ctx ) => {
208225 const isDevEnv = isDev ( ) ;
226+ const isProductionEnv = getNodeEnv ( ) === "production" ;
227+ const envStartEngine = getRivetRunEngine ( ) ;
228+ const explicitStartEngine =
229+ config . startEngine !== undefined || envStartEngine ;
230+ let startEngine = true ;
231+ let runtimeMode : RuntimeMode = "envoy" ;
209232
210233 // Parse endpoint string (env var fallback is applied via transform above)
211234 const parsedEndpoint = config . endpoint
@@ -217,16 +240,55 @@ export const RegistryConfigSchema = z
217240 } )
218241 : undefined ;
219242
220- // Can't start a local engine and connect to a remote endpoint.
221- if ( config . startEngine && parsedEndpoint ) {
243+ if ( isProductionEnv ) {
244+ startEngine = false ;
245+ runtimeMode = "serverless" ;
246+ }
247+
248+ if ( parsedEndpoint ) {
249+ startEngine = false ;
250+ runtimeMode = "serverless" ;
251+ }
252+
253+ if ( config . mode === "envoy" ) {
254+ startEngine = false ;
255+ runtimeMode = "envoy" ;
256+ } else if ( config . mode === "serverless" ) {
257+ startEngine = false ;
258+ runtimeMode = "serverless" ;
259+ }
260+
261+ if ( explicitStartEngine ) {
262+ startEngine = config . startEngine ?? envStartEngine ;
263+ if ( startEngine ) {
264+ runtimeMode = "envoy" ;
265+ }
266+ }
267+
268+ if ( explicitStartEngine && startEngine && parsedEndpoint ) {
269+ ctx . addIssue ( {
270+ code : "custom" ,
271+ message :
272+ "cannot specify startEngine: true with a Rivet endpoint" ,
273+ } ) ;
274+ }
275+
276+ if ( ! startEngine && ! parsedEndpoint ) {
222277 ctx . addIssue ( {
223278 code : "custom" ,
224- message : "cannot specify both startEngine and endpoint" ,
279+ message : "Rivet endpoint is required when startEngine is false" ,
280+ } ) ;
281+ }
282+
283+ if ( runtimeMode === "serverless" && startEngine ) {
284+ ctx . addIssue ( {
285+ code : "custom" ,
286+ message : "serverless runtime cannot start the local engine" ,
225287 } ) ;
226288 }
227289
228290 // configurePool requires an engine (via endpoint or startEngine).
229- if ( config . configurePool && ! parsedEndpoint && ! config . startEngine ) {
291+ if ( config . configurePool && ! parsedEndpoint && ! startEngine ) {
230292 ctx . addIssue ( {
231293 code : "custom" ,
232294 message :
@@ -236,12 +298,11 @@ export const RegistryConfigSchema = z
236298
237299 // Flatten the endpoint and apply defaults for namespace/token
238300 // If startEngine is enabled, set endpoint to the engine endpoint.
239- const endpoint = config . startEngine
301+ const endpoint = startEngine
240302 ? ENGINE_ENDPOINT
241- : ( parsedEndpoint ?. endpoint ??
242- ( isDevEnv ? ENGINE_ENDPOINT : undefined ) ) ;
303+ : parsedEndpoint ?. endpoint ;
243304 const validateServerlessEndpoint = Boolean (
244- config . startEngine || parsedEndpoint ,
305+ startEngine || parsedEndpoint ,
245306 ) ;
246307 // Namespace priority: parsed from endpoint URL > config value (includes env var) > "default"
247308 const namespace =
@@ -272,7 +333,7 @@ export const RegistryConfigSchema = z
272333 // In dev mode, clients connect directly to the local Rivet Engine.
273334 const publicEndpoint =
274335 parsedPublicEndpoint ?. endpoint ??
275- ( isDevEnv && config . startEngine ? ENGINE_ENDPOINT : undefined ) ;
336+ ( isDevEnv && startEngine ? ENGINE_ENDPOINT : undefined ) ;
276337 // We extract publicNamespace to validate that it matches the backend
277338 // namespace (see validation above), not for functional use.
278339 const publicNamespace = parsedPublicEndpoint ?. namespace ;
@@ -282,6 +343,8 @@ export const RegistryConfigSchema = z
282343 // If endpoint is set or starting the engine, we'll use the engine driver.
283344 return {
284345 ...config ,
346+ startEngine,
347+ runtimeMode,
285348 endpoint,
286349 namespace,
287350 token,
@@ -516,11 +579,14 @@ export const DocRegistryConfigSchema = z
516579 . string ( )
517580 . optional ( )
518581 . describe ( "Host to bind the local HTTP server to." ) ,
582+ mode : RuntimeModeSchema . optional ( ) . describe (
583+ "Runtime mode for registry.start(). Defaults to 'envoy' for local development and 'serverless' when a Rivet endpoint or production environment is configured." ,
584+ ) ,
519585 startEngine : z
520586 . boolean ( )
521587 . optional ( )
522588 . describe (
523- "Starts the full Rust engine process locally. Default: false" ,
589+ "Starts the full Rust engine process locally. Defaults to true for local development and false when a Rivet endpoint or production environment is configured. " ,
524590 ) ,
525591 engineVersion : z
526592 . string ( )
0 commit comments