Configuration
What it is
Section titled “What it is”gisila is configured in code, at two levels:
AppConfigholds everything app-wide: logging, the authenticator and permission resolver, the database and cache providers, CORS, security headers, compression, ETags, rate limits, timeouts, body limits, health checks and metrics. You pass one toGisilaApp.RouteConfigholds what can differ per route: rate limit, timeout, body cap, allowed content types, CORS overrides, extra middleware and guards. It is layered:AppConfig.defaultRouteConfig, then@UseRouteConfigon the controller, then the guards, then the annotation’sconfig:.
There is no configuration file format and no environment loader in gisila. You read Platform.environment (or any config package) in main() and build the objects.
When to use it
Section titled “When to use it”AppConfigfor anything that should hold for every request, and for wiring pluggable parts (authenticator, database, cache, logger).defaultRouteConfigfor a baseline every route should have, such as a 10-second timeout.@UseRouteConfigfor a controller with different needs (an admin area with tighter limits).config:on an annotation for a single route (login rate limit, large upload cap).
When not to use it
Section titled “When not to use it”- Don’t put secrets in
RouteConfigliterals or annotations: they are compiled into generated code. Read secrets from the environment intoAppConfigobjects at startup. - Don’t use
AppConfig.extraas a general settings bag for services. Register a typed settings object as a singleton instead.
How it works
Section titled “How it works”AppConfig is read once, when buildHandler() assembles the pipeline; changing it afterwards has no effect. Route configs are constants generated at build time and merged with defaultRouteConfig when each route is attached.
Merging rules (RouteConfig.merge): scalar fields from the more specific layer win when set; permission, any-permission and role sets are unioned; middleware lists are concatenated; @Public clears guards.
App-wide and per-route limits are separate middleware, so both apply:
| Setting | App-wide | Per route | Effective |
|---|---|---|---|
| Rate limit | AppConfig.rateLimit |
RouteConfig.rateLimit |
Must pass both. Each route’s limiter has its own buckets. |
| Timeout | AppConfig.requestTimeout |
RouteConfig.timeout |
The shorter one. |
| Body size | AppConfig.maxRequestBodyBytes |
RouteConfig.maxBodyBytes |
The smaller one. |
| CORS | AppConfig.cors |
RouteConfig.cors |
The app-wide one answers pre-flights. |
Defaults worth changing for production: set maxRequestBodyBytes (unlimited by default), requestTimeout, logFormat: LogFormat.json, a real authenticator, cors if browsers call you, and trustedProxies if your load balancer is not on the same host.
Example
Section titled “Example”import 'dart:io';
import 'package:gisila/gisila.dart';
class Settings { Settings.fromEnv(Map<String, String> env) : production = env['APP_ENV'] == 'production', port = int.parse(env['PORT'] ?? '8080'), jwtSecret = env['JWT_SECRET'] ?? (throw StateError('JWT_SECRET is not set')), frontendOrigin = env['FRONTEND_ORIGIN'] ?? 'http://localhost:5173';
final bool production; final int port; final String jwtSecret; final String frontendOrigin;}
Future<void> main() async { final s = Settings.fromEnv(Platform.environment); final app = GisilaApp( config: AppConfig( logFormat: s.production ? LogFormat.json : LogFormat.text, authenticator: JwtAuthenticator(key: JwtKey.hmac(s.jwtSecret), algorithms: {'HS256'}), cors: CorsConfig(allowedOrigins: {s.frontendOrigin}, allowCredentials: true), rateLimit: const RateLimitConfig(requestsPerMinute: 600), defaultRouteConfig: const RouteConfig(timeout: Duration(seconds: 10)), maxRequestBodyBytes: 1024 * 1024, healthCheckPath: '/healthz', ), ); app.registerSingleton<Settings>(s); // register services and controllers ... await app.start(port: s.port, stopOnSignals: true);}