Blog

All Blog Posts  |  Next Post  |  Previous Post

One Build, Every Environment: Configuration in TMS BIZ

Tuesday, October 13, 2026

Open the source code of almost any Delphi server and you will find a line like this one:

Server.BaseUrl := 'http://localhost:2001/tms/xdata';

It works on your machine. Then the server has to go to the test machine, where the port is different, and to the customer, where the database is on another host and has another password. And every Delphi developer has solved that the same way, at least once:

  • A {$IFDEF DEBUG} with one set of values for you and another for production.
  • A hand-written unit that reads an INI file into global variables - and that grows with every new setting.
  • A production password sitting in a file that got committed to version control.
  • A rebuild "just to change the URL".
  • A server that starts fine and fails three hours later, at the first request that needs the setting somebody misspelled.

None of that is your application. It is plumbing, and it is the same plumbing in every project.

TMS BIZ takes it off your hands. The application configuration feature lets you keep the settings in a JSON file beside the executable, override any of them with environment variables, and read them from anywhere in your code - with no setup code at all.


A quick refresher

TMS BIZ is a suite of Delphi libraries for building backends. The configuration feature lives in the TMS BIZ Core Library (BCL), the foundation every other BIZ product is built on, so it is available whichever product you use: TMS Sparkle, a high-performance HTTP server framework; TMS XData, a framework for building REST APIs; TMS Aurelius, an ORM framework; TMS Sphinx, an OAuth 2.0 and OpenID Connect server framework; and TMS RemoteDB, a framework for remote database access over HTTP.

It is not tied to servers, either. A VCL or FireMonkey client that needs to know the address of the API it talks to can use it in exactly the same way.


Two lines, no setup

Say your executable is MyServer.exe. Put a file named MyServer.json beside it:

{
  "BaseUrl": "http://localhost:2001/tms/xdata",
  "AllowedOrigins": ["https://app.example.com"],
  "Database": {
    "Hostname": "localhost",
    "Port": 5432,
    "Name": "sales",
    "User": "app",
    "Password": "dev-password"
  }
}

And read it:

uses
  Bcl.Configuration;

procedure TServerModule.DataModuleCreate(Sender: TObject);
begin
  Server.BaseUrl := TConfiguration.Current.RequireString('BaseUrl');
  Server.Dispatcher := SocketDispatcher;
  SocketDispatcher.Start;
end;

That is everything. There is no file name in the code, no object to create, no object to free, no loading step. TConfiguration.Current is the configuration of the application: it loads itself the first time something reads it, and from then on it is available everywhere - a data module, a form, a service implementation, a background thread.

Values come back typed, and nested ones are addressed with a dot:

var
  Config: IConfiguration;
  Origin: string;
begin
  Config := TConfiguration.Current;

  // Must be there: the application does not start without it.
  Connection.Server := Config.RequireString('Database.Hostname');

  // May be absent: the default is used then.
  Connection.Port := Config.GetInteger('Database.Port', 5432);

  for Origin in Config.GetStrings('AllowedOrigins') do
    AllowOrigin(Origin);
end;

So far this is a nicer INI file. The interesting part is what happens when the application leaves your machine.


Same executable, different environment

The JSON file holds the settings that are the same everywhere. Each deployment then overrides only what differs, with environment variables named after the executable:


Environment variableOverrides
MYSERVER_BASEURLBaseUrl
MYSERVER_DATABASE__HOSTNAMEDatabase.Hostname
MYSERVER_DATABASE__PASSWORDDatabase.Password


The prefix is the name of the executable, and two underscores go one level down. On the test machine:

set MYSERVER_DATABASE__HOSTNAME=db.test.internal
MyServer.exe

Nothing was rebuilt, and no file was edited. The binary that ran on your machine is, byte for byte, the one that runs in test and the one that runs in production. What you tested is what you ship - which is the whole point of testing it.

This is not a TMS invention. It is how the rest of the industry has configured server applications for years - the "config" rule of the Twelve-Factor App - and now a Delphi server follows it too.


Docker, Kubernetes, systemd: it just works there

Because environment variables are the standard, every modern deployment platform already knows how to supply them. A Delphi server that reads its configuration this way runs on all of them with no extra code.

In a Docker container:

docker run -e MYSERVER_DATABASE__HOSTNAME=db.internal -e MYSERVER_DATABASE__PASSWORD=... myserver

As a systemd daemon on Linux, in the [Service] section of the unit file:

Environment=MYSERVER_CONFIG=/etc/myserver/myserver.json
Environment=MYSERVER_DATABASE__HOSTNAME=db.internal

Notice MYSERVER_CONFIG in that last example: it tells the application to load its JSON file from somewhere else. During development the file sits beside the executable; in production the administrator keeps it in /etc, where configuration files belong, and your code doesn't know the difference.

The same applies to Kubernetes, to cloud hosting platforms and to CI pipelines: they all hand settings to a process as environment variables, and your server picks them up.


Passwords out of the file

Look again at the JSON file above. It has a password in it. That's acceptable for the development database on your own machine. It is not acceptable for production: that file ends up in version control, in backups, in the zip somebody e-mails to a colleague.

With configuration, the production password is simply not in the file. It comes from an environment variable, which is how Docker and Kubernetes deliver secrets. Or, if your company keeps secrets in a vault - HashiCorp Vault, AWS Parameter Store, Azure Key Vault - you fetch it at startup and put it into the configuration yourself:

Config.SetValue('Database.Password', ReadSecret('sales-db'), 'the secret store');

ReadSecret is your own function, whatever your vault requires. The rest of the application doesn't change: it still reads Database.Password, and has no idea where the value came from.


Settings as a class, validated before the server starts

A handful of settings are fine to read one by one. Twenty are not. For a real application, declare the settings as a plain Delphi class:

uses
  Bcl.Configuration, Bcl.Configuration.Binder, Bcl.Validation.Attributes;

type
  TDatabaseOptions = class
  private
    [Required]
    FHostname: string;
    [Range(1, 65535)]
    FPort: Integer;
    [Required]
    FName: string;
    FUser: string;
    FPassword: string;
  public
    constructor Create;
    property Hostname: string read FHostname;
    property Port: Integer read FPort;
    property Name: string read FName;
    property User: string read FUser;
    property Password: string read FPassword;
  end;

  TServerOptions = class
  private
    [Required]
    FBaseUrl: string;
    FAllowedOrigins: TArray<string>;
    [Required]
    FDatabase: TDatabaseOptions;
  public
    destructor Destroy; override;
    property BaseUrl: string read FBaseUrl;
    property AllowedOrigins: TArray<string> read FAllowedOrigins;
    property Database: TDatabaseOptions read FDatabase;
  end;

constructor TDatabaseOptions.Create;
begin
  inherited Create;
  FPort := 5432; // the default, used when the setting is absent
end;

destructor TServerOptions.Destroy;
begin
  FDatabase.Free;
  inherited;
end;

And fill it with one call:

Options := TConfigurationBinder.Bind<TServerOptions>(TConfiguration.Current);

That is binding, and it gives you three things at once:

  • Compile-time checked settings. The rest of your code uses Options.Database.Hostname. No string keys scattered around, no typos the compiler cannot see, and code completion knows every setting you have.
  • Defaults in one place. A setting that is not in the file keeps the value the constructor gave it.
  • Rules. [Required], [Range], [MinLength], [RegularExpression] and the other validation attributes are checked as the object is filled.

Which brings us to the benefit that will save you the most time.


Mistakes found at startup, not at 3 a.m.

A misconfigured server is at its most dangerous when it starts anyway. It opens its port, reports that all is well, and fails later - at the first request that touches the bad setting, with an error that says nothing about configuration.

With binding, a server with a bad configuration does not start. And it tells the administrator exactly what to fix:

The configuration has 3 errors:
  Configuration key "Database.Port" from environment variable "MYSERVER_DATABASE__PORT" must be an integer.
  Configuration key "Database.Hostname" is required but was not found.
  Configuration key "BaseUrl" is required but was not found.

Three details in that message are deliberate:

  • It names the source. Not just "invalid port", but which environment variable or which file the bad value came from.
  • It lists everything. All the problems in a single run, so a deployment with three mistakes is fixed in one round instead of three.
  • It never shows the value. The value might be a password, and error messages end up in log files.

Think about who reads that message. Usually it is not you: it is an administrator at the customer, or a support colleague, on a machine you have no access to. A message like the one above is a problem they solve themselves. "Access violation at address 00405F2C" is a support ticket.


"Why is it connecting to that database?"

Once settings can come from more than one place, sooner or later a server behaves differently from what its file says - because somebody set an environment variable on that machine two years ago. Describe answers the question. Write its result to the log when the server starts:

for Line in TConfiguration.Current.Describe do
  Writeln(Line);
BaseUrl: file "/etc/myserver/myserver.json"
Database.Hostname: environment variable "MYSERVER_DATABASE__HOSTNAME"
Database.Port: file "/etc/myserver/myserver.json"
Database.Password: the secret store

One line per setting, saying where its value came from - and, again, never the value itself.


In a Sparkle or XData server

A server built on the hosting layer of TMS Sparkle - one executable that runs as a console application, a Windows service or a Linux daemon - needs nothing more than what you have seen. Read the settings in the OnCreate of the server data module, as in the first example. That code runs as part of the startup, so a configuration error is treated like any other startup failure: nothing is started, the error is logged, and the process exits with code 1, where the Service Control Manager, systemd and your deployment script can all see it.

If you want sources other than the conventional file and environment variables - the vault from the example above, for instance - the host has a place for that:

uses
  Bcl.Configuration, Sparkle.Host;

begin
  CreateHost
    .Configuration(
      procedure(Config: IConfigurationRoot)
      begin
        Config.AddDefaults;
        Config.SetValue('Database.Password', ReadSecret('sales-db'),
          'the secret store');
      end)
    .Add(TServerModule)
    .Run;
end.

AddDefaults adds the conventional file and environment variables, and whatever you add after it wins. What IHostBuilder.Configuration builds becomes TConfiguration.Current for the whole application. The details are in the Configuration section of the Hosting chapter.


Where else you can use it

Any Delphi application has settings that do not belong in the code:

  • A REST API server: the URL it listens on, database connections, allowed CORS origins, the key that signs its JWT tokens.
  • A multi-tenant server: the list of databases, one per customer or per shard, that differs on every installation.
  • A desktop client: the address of the API, which is one thing at your office and another at each customer site.
  • A Windows service or background worker: polling intervals, folders to watch, the SMTP server it sends mail through.
  • A command-line tool run by a CI pipeline, which receives everything through environment variables.

In all of them the code is the same two lines, and the application needs no setup.

One thing to know: the configuration is read once and stays as it was read. To apply a changed setting, restart the application.


Conclusion

Configuration is code nobody wants to write and everybody ends up writing. With the application configuration in TMS BIZ you don't:

  • No configuration code: a JSON file beside the executable and TConfiguration.Current anywhere in the application.
  • One executable for every environment: each deployment overrides only what differs, and nothing is rebuilt.
  • Ready for Docker, Kubernetes and systemd, because it reads settings the way those platforms supply them.
  • Secrets stay out of files and out of version control - and out of error messages and logs.
  • Settings as a typed, validated class, with defaults in its constructor.
  • A misconfigured server does not start, and says exactly which setting is wrong and where it came from.

To learn more, read the Configuration chapter of the TMS BIZ Core Library documentation, the Configuration section of the Sparkle Hosting chapter, and the release notes of BCL and Sparkle. And visit the product pages of TMS Sparkle and TMS XData, both part of TMS BIZ.



Wagner Landgraf




This blog post has not received any comments yet.



Add a new comment

You will receive a confirmation mail with a link to validate your comment, please use a valid email address.
All fields are required.



All Blog Posts  |  Next Post  |  Previous Post