Skip to content

Configuration

gisila is configured in code, at two levels:

  • AppConfig holds 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 to GisilaApp.
  • RouteConfig holds 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 @UseRouteConfig on the controller, then the guards, then the annotation’s config:.

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.

  • AppConfig for anything that should hold for every request, and for wiring pluggable parts (authenticator, database, cache, logger).
  • defaultRouteConfig for a baseline every route should have, such as a 10-second timeout.
  • @UseRouteConfig for 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).
  • Don’t put secrets in RouteConfig literals or annotations: they are compiled into generated code. Read secrets from the environment into AppConfig objects at startup.
  • Don’t use AppConfig.extra as a general settings bag for services. Register a typed settings object as a singleton instead.

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.

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);
}