Blog
All Blog Posts | Next Post | Previous Post
TMS BIZ October 2026: Multi-Tenancy, One-Executable Servers, Configuration and More
Today
Three things take up a surprising share of every Delphi backend project, and none of them is business logic: deciding where each customer's data lives, turning a server you can debug into one you can deploy as a Windows service or a Linux daemon, and getting its settings (URLs, database connections, passwords) right on every machine it runs on. Most teams end up writing that plumbing themselves, and each one writes it slightly differently.
The latest updates of TMS BIZ ship all three, built in: a tenant catalog with database sharding for TMS Aurelius and TMS XData, a hosting layer in TMS Sparkle that makes one executable run as a console application, a Windows service or a systemd daemon, and an application configuration that lets that same executable run unchanged in development, test and production.
Around them come background jobs with cancellation tokens, JWT signing with OpenSSL 3 together with a round of JWT security fixes, new server wizards, shared endpoints for XData service contracts, non-blocking Discord and Slack logging, and improvements and fixes across Sphinx, Sparkle, Aurelius and the core library.
Each of the three headline features has its own in-depth article, so this post gives you the short version and a link, and then covers everything else.
1. Multi-tenancy with database sharding (TMS Aurelius and TMS XData)
TMS XData is a framework for building REST APIs in Delphi, and TMS Aurelius is the ORM framework it uses to query and persist your entities.
A SaaS backend usually starts with a TenantId column in a single database. As it grows, some customers need a dedicated database, tenants have to be spread over several servers, and suspending a customer must take effect immediately. This release covers all of that out of the box:
- A tenant catalog that maps each tenant to the database (the shard) that hosts it, with a database-backed implementation and the new
TAureliusTenantCatalogcomponent. - The new
TXDataTenantConnectionPoolcomponent, which routes every request to its tenant's shard, pools connections per shard and enables the row-level tenant filter. Your endpoints don't change. - Proper HTTP errors for unknown, suspended or mismatched tenants, tenants resolved from token claims, API keys or domains, and schema updates on all shards.
- Idempotent tenant provisioning with pluggable shard allocation, and
TTenantScopeto run background jobs as a given tenant.
Day-to-day operations become one-liners, without restarting the server:
TenantCatalog.AddTenant('globex', 'Globex'); // allocates a shard, provisions, activates
TenantCatalog.SuspendTenant('acme'); // takes effect on the next request
TenantCatalog.MoveTenantToShard('acme', 'shard-b'); // re-route after copying the data
The MultiTenancy demo folder of TMS XData now includes a MultiTenantCatalog project with the full setup, and the new components have their own icons in the IDE component palette.
If you tried the tenant catalog in Aurelius 5.29: the VALUE field of the tc_identifiers table is renamed to ID_VALUE since 5.30, because VALUE is a reserved word in some databases.
For the complete walkthrough, read:
👉 Multi-Tenancy and Database Sharding in TMS XData
2. One executable: console, Windows service or Linux daemon (TMS Sparkle)
TMS Sparkle is the high-performance HTTP server framework underneath XData, TMS RemoteDB and TMS Sphinx.
The new Sparkle.Host unit turns a console program into a server host. The same binary runs as an interactive console application while you develop, as a Windows service in production, or as a systemd daemon on Linux. The mode is picked from the command line at runtime, so there is no separate service project and no conditional defines. The program file is a single call:
program MyServer;
{$APPTYPE CONSOLE}
uses
Sparkle.Host,
ServerModuleUnit in 'ServerModuleUnit.pas' {ServerModule: TDataModule};
begin
RunHost(TServerModule);
end.
Deployment is built into the executable: MyServer.exe --install registers the Windows service, and ./myserver --systemd-unit prints the systemd unit file. The host also runs background jobs in the same process (see section 4), and takes care of graceful shutdown and of reload requests from the service manager. A complete example ships in the new demos/host folder.
The Sparkle.App units are now deprecated in favor of Sparkle.Host. They keep compiling and working, and moving an existing server over usually takes a few lines. All the details, including migration:
👉 Server Hosting in TMS Sparkle: Console, Windows Service or Linux Daemon
3. One build, every environment: application configuration (BCL and TMS Sparkle)
A server that runs on your machine, on the test server and at the customer needs different URLs, database hosts and passwords in each place. The usual answers are a {$IFDEF DEBUG}, a hand-written INI reader, or a rebuild "just to change the URL".
The TMS BIZ Core Library (BCL), the foundation shared by all TMS BIZ products, now has a built-in application configuration. Put a MyServer.json file beside MyServer.exe:
{
"BaseUrl": "http://localhost:2001/tms/xdata",
"Database": {
"Hostname": "localhost",
"Port": 5432
}
}
And read it from anywhere, with no setup code:
uses
Bcl.Configuration;
procedure TServerModule.DataModuleCreate(Sender: TObject);
begin
Server.BaseUrl := TConfiguration.Current.RequireString('BaseUrl');
Connection.Server := TConfiguration.Current.RequireString('Database.Hostname');
Connection.Port := TConfiguration.Current.GetInteger('Database.Port', 5432);
end;
TConfiguration.Current loads itself the first time something reads it. Each deployment then overrides only what differs with environment variables named after the executable, such as MYSERVER_DATABASE__HOSTNAME, which is exactly how Docker, Kubernetes and systemd hand settings to a process. The binary you tested is the one you ship, and the production password never has to be in a file.
For larger applications, settings can be bound to a plain Delphi class validated with attributes like [Required] and [Range]. In a server built on Sparkle.Host, a missing or invalid setting keeps the server from starting, with a message that names the setting and where its value came from. The full story, including secrets, binding and the Describe diagnostics:
👉 One Build, Every Environment: Configuration in TMS BIZ
4. Background jobs that stop when the server stops (TMS Sparkle and BCL)
Almost every server has work to do outside of any request: send pending invoices every 15 minutes, clean up expired sessions, refresh a cache. The code is usually a thread with a Sleep loop, and it has two typical problems: an exception kills the thread silently, and a stop request waits until the current sleep or batch is over.
The new TJobWorker class solves both. It runs a job when it starts, again Period seconds after each run, and whenever you call Trigger. Runs never overlap, a failed run is logged and the worker goes on, and the job receives a stop token it can pass down into its work:
uses
Bcl.Cancellation,
Sparkle.Host.Jobs;
type
TInvoiceSyncJob = class(TJobWorker)
protected
procedure ProcessJob(const AToken: ICancellationToken); override;
public
constructor Create; override;
end;
constructor TInvoiceSyncJob.Create;
begin
inherited;
Period := 15 * 60; // seconds
end;
procedure TInvoiceSyncJob.ProcessJob(const AToken: ICancellationToken);
var
InvoiceId: Integer;
begin
for InvoiceId in GetPendingInvoiceIds do
begin
// The server is stopping: raises EOperationCancelled and ends the run cleanly
AToken.ThrowIfCancellationRequested;
SendInvoice(InvoiceId);
end;
end;
Add the job to the host next to your server, keeping a reference if you want to trigger it or check on it:
uses
Sparkle.Host,
Sparkle.Host.Contracts;
begin
// InvoiceSync is a global variable declared in a unit your endpoints can use
InvoiceSync := TInvoiceSyncJob.Create;
CreateHost
.Add(TServerModule)
.Add(InvoiceSync as IHostedService) // the host owns and frees it
.Run;
end.
From an endpoint, InvoiceSync.Trigger asks for a run right away, for example after new invoices arrived. For a health check, State returns a thread-safe snapshot of the last run: run and failure counts, duration, last error and when the next run is due. TJobWorker also works without a host: call Start and Stop yourself. The previous TJobRunner class is deprecated in favor of TJobWorker, and keeps working unchanged.
The stop token is built on the new cancellation tokens of the BCL. The Bcl.Cancellation unit provides cooperative cancellation for any long-running code, hosted or not: the caller holds a TCancellationTokenSource and calls Cancel, and the work receives an ICancellationToken it can check, wait on, or register a callback with to wake up a blocking wait. Tokens combine with time limits, so a job can give up when the server stops or after five minutes, whichever comes first:
procedure TInvoiceSyncJob.ProcessJob(const AToken: ICancellationToken);
var
Source: ICancellationTokenSource;
begin
Source := TCancellationTokenSource.CreateLinked([AToken]);
Source.CancelAfter(5 * 60 * 1000); // milliseconds
SyncInvoices(Source.Token);
end;
The worker stop token IStopToken that custom workers receive is now a cancellation token as well, so the same code works everywhere. See the Background Jobs and Cancellation chapters for the details.
5. JWT tokens with OpenSSL 3, JSON Web Keys and a security round (BCL, Sparkle and XData)
JSON Web Tokens (JWT) are how a Sparkle or XData server knows who is calling it. Tokens signed with a shared secret (HS256) always worked anywhere, but tokens signed with RSA or elliptic-curve keys (RS256, ES256) needed the OpenSSL 1.0 libraries, which reached end of life in 2019 and are missing from current Windows, Linux and macOS systems.
Now, add one unit to your project:
uses
Bcl.OpenSsl.Jose; // registers the OpenSSL 3 signing providers, nothing else to call
With Bcl.OpenSsl.Jose in place, RSA (RS256/RS384/RS512) and ECDSA (ES256/ES256K/ES384/ES512) tokens are signed and validated with OpenSSL 3, the version modern systems actually ship, with no dependency on Indy. That covers the JWT middleware of Sparkle and XData servers as well. Applications that still deploy OpenSSL 1.0 keep working: the previous implementation is used whenever OpenSSL 3 cannot be loaded. If you were using TSparkleJoseProviders from the Sparkle.OpenSsl.Jose unit, it is deprecated in favor of the new unit, and keeps working.
Two additions come with it:
- RSASSA-PSS signing (PS256, PS384, PS512), the modern RSA signature scheme, required by some identity providers.
- JSON Web Keys (JWK), through
TJSONWebKeyandTJSONWebKeySet. A JWK Set is how an identity server publishes the public keys it signs tokens with. You can read one, let the library pick the key a token was signed with, convert keys between JWK and PEM, and compute the standard key thumbprint.
For example, to check a token issued by an identity server, given the JWK Set it publishes at its jwks_uri:
uses
Bcl.OpenSsl.Jose, Bcl.Jose.Core.Builder, Bcl.Jose.Core.JWK, Bcl.Jose.Core.JWT;
function GetTokenSubject(const JwksJson, AccessToken: string): string;
var
KeySet: TJSONWebKeySet;
Token: TJWT;
begin
KeySet := TJSONWebKeySet.FromJSON(JwksJson);
try
// Picks the key named by the token header (kid) and checks the signature.
// Raises an exception when the token does not verify.
Token := TJOSE.VerifyOrRaise(KeySet, AccessToken);
try
Result := Token.Claims.Subject;
finally
Token.Free;
end;
finally
KeySet.Free;
end;
end;
The new VerifyOrRaise checks the signature only, so a token it returns is always one that checked out; claims such as the expiration are still yours to validate, as the JWT middleware does.
Updating the library, which is based on the Delphi JOSE JWT library, also closed several weaknesses in token validation. Among them:
- Unsigned tokens (
alg=none) were accepted by a consumer that had not listed the algorithms it expects. - A consumer kept alive for a long time, like the one in a running server, went on accepting expired tokens.
- A PEM public key was accepted as an HMAC secret, so anyone holding the public key of an RS256 issuer could forge an HS256 token.
- Signatures were compared in non-constant time, and validation error messages could carry the token's claims into log files.
Each fix makes the library reject tokens it used to accept, so check the Breaking Changes page if your application relied on any of them. The full list is in the BCL release notes.
6. New server wizards, and one TMS BIZ gallery category
The fastest way to start with the hosting layer is not to write the program file at all. Three new IDE wizards create a ready-to-run hosted server, targeting Win32, Win64 and Linux64:
- TMS Sparkle Server: a server built on the Sparkle design-time components, with the socket server dispatcher active by default and the Http.sys dispatcher ready for Windows production.
- TMS XData Server: an XData server with the components already dropped and wired on a data module.
- TMS RemoteDB Server: the same for a RemoteDB server.
The generated program builds the host with CreateHost.Add(TServerModule).Run, so adding a background job or another module later is one more .Add line.
Finding wizards is also easier. In File > New > Other, all TMS BIZ wizards are now grouped under a single TMS BIZ category. A few were renamed along the way: the XData service wizard is now TMS XData Service Operations, and the TMS Web Core scaffolding wizard is TMS XData Web Core Sample Application.
The previous server wizards are not gone. "TMS XData VCL Server", "TMS XData Server Application", "TMS RemoteDB VCL Server", "TMS RemoteDB Server Application", the older Sparkle server wizards and the "TMS Aurelius DBConnection" wizard are in a new TMS BIZ (Deprecated) category and keep working. For new projects, use the new server wizards, and for Aurelius simply drop a TAureliusConnection component on your own data module.
See the getting-started guides for the XData Server wizard and the RemoteDB Server wizard.
7. Shared endpoints for XData service contracts (TMS XData)
In XData, the endpoints of your API are declared as methods of a service contract interface. When several services share endpoints, such as a health check or a list of recent changes, the natural way to write them is an ancestor interface:
type
// Endpoints that every service of the API offers
IServiceBase = interface(IInvokable)
['{6F1C2B8E-3A4D-4E5F-9A7B-1C2D3E4F5A6B}']
/// <summary>Checks that the service is up</summary>
[HttpGet] function Ping: string;
/// <summary>Lists the most recent changes made through the service</summary>
/// <param name="Top">Maximum number of entries to return</param>
[HttpGet] function RecentChanges([Range(1, 100)] Top: Integer): TList<TChange>;
end;
[ServiceContract]
[ValidateParams] // also validates the parameters of the inherited endpoints
IOrderService = interface(IServiceBase)
['{0B4E7D21-5C6A-4F8B-8D9E-2A3B4C5D6E7F}']
[HttpGet] function GetOrder(Id: Integer): TOrder;
end;
[ServiceContract]
[ValidateParams]
ICustomerService = interface(IServiceBase)
['{9C8D7E6F-1A2B-4C3D-8E9F-0A1B2C3D4E5F}']
[HttpGet] function GetCustomer(Id: Integer): TCustomer;
end;
Both services publish Ping and RecentChanges, and inheriting service contracts now works the way you'd expect:
- The XML documentation comments of the ancestor interface show up in the OpenAPI document and Swagger UI, so you document a shared endpoint once.
- Operation ids are prefixed with the service contract (
IOrderService.Ping,ICustomerService.Ping), keeping them unique for client code generators. - Attributes on the contract interface, such as
[ValidateParams], apply to the inherited endpoints as well. - A method redeclared in a descendant interface, usually to change its attributes or documentation, is published once, from the most derived declaration. Previously it was published twice and every request to it failed.
The TMS XData documentation also has a new API reference, with pages generated from the source code documentation. It starts with the multi-tenancy classes, such as TXDataTenantConnectionPool, and more classes will follow. Guide chapters link to it, so you can jump from an explanation straight to the exact property or method.
8. Large compressed responses over HTTP/2 (TMS Sparkle)
On current Windows versions, an HTTPS server based on http.sys talks HTTP/2 with browsers by default. Large responses sent without a Content-Length header failed there: the browser reported ERR_HTTP2_PROTOCOL_ERROR, and the server an "Incorrect function" error. That is exactly what TCompressMiddleware produces, and streamed content too.
This is fixed, so compression can stay on for HTTPS clients. In addition, THttpServerRequest.Protocol now reports HTTP/2 (or HTTP/3) for those requests, instead of HTTP/1.1.
9. Inspecting the servers of a dispatcher (TMS Sparkle)
When you build a server with the Sparkle design-time components, server components (such as XData or Sphinx servers) are connected to a dispatcher component, which actually listens for requests. Until now, there was no public way to ask a dispatcher which servers it serves.
The new ServerCount and Servers properties of TSparkleDispatcher list them. That's handy, for instance, to log the addresses a server is listening on at startup:
procedure TServerModule.LogServerAddresses(Dispatcher: TSparkleDispatcher);
var
I: Integer;
begin
for I := 0 to Dispatcher.ServerCount - 1 do
WriteLn('Listening on: ', Dispatcher.Servers[I].BaseUrl);
end;
10. One external identity per user, and more providers that just work (TMS Sphinx)
Sphinx is an OAuth 2.0 and OpenID Connect server framework for Delphi. Since the July release it can act as an identity broker, letting users sign in with Google, Microsoft and other providers, and linking each external identity to a local user through IUserManager.AddLogin.
That link is now protected: an external identity can belong to only one local user. If you call AddLogin for an identity already linked to a different user, Sphinx raises an ESphinxErrorException with the LoginAlreadyAssociated error code, instead of creating a second link that would make the identity ambiguous. For example, in a custom OnExternalSignIn handler:
procedure TSphinxServerModule.SphinxConfig1ExternalSignIn(Sender: TObject;
Args: TExternalSignInArgs);
var
User: TUser;
begin
User := Args.UserManager.FindByLogin(Args.ProviderName, Args.Subject);
if (User = nil) and Args.EmailVerified then
begin
User := Args.UserManager.FindByEmail(Args.Email);
if User <> nil then
try
Args.UserManager.AddLogin(User, Args.ProviderName, Args.Subject);
except
on E: ESphinxErrorException do
if E.ErrorCode = 'LoginAlreadyAssociated' then
begin
Args.Reject('This account is already linked to another user');
Exit;
end
else
raise;
end;
end;
Args.User := User;
end;
To move an identity to another user, unlink it first with IUserManager.RemoveLogin.
Connecting to more upstream providers also got easier. When Sphinx exchanges the authorization code, it authenticates itself at the provider's token endpoint. Some providers accept the client credentials only in the request body (client_secret_post) instead of an HTTP Basic header. The new TokenEndpointAuthMethod property detects that from the provider's discovery document, and you can also set it explicitly:
Provider := SphinxConfig1.ExternalProviders.Add;
Provider.Name := 'partner';
Provider.Authority := 'https://login.partner.example.com';
Provider.ClientId := 'your-client-id';
Provider.ClientSecret := 'your-client-secret';
Provider.TokenEndpointAuthMethod := TExternalTokenEndpointAuthMethod.ClientSecretPost;
The same property is available in TOidcClient, the OpenID Connect client you use in Delphi applications. Also in this round:
TOidcClientaccepts anexpires_invalue sent as a JSON string, as Microsoft does. It was ignored before, so such tokens were considered to never expire.- The logout endpoint no longer shows an error page for a malformed
id_token_hint: it proceeds with a regular logout, asking the user for confirmation. It also no longer skips that confirmation when the hint has an empty audience and the application accepts an empty client id. - Updating the database schema no longer fails on Firebird databases using UTF8 at the default page size, where the index on the
sx_user_loginstable exceeded Firebird's maximum key size.
11. Logging to Discord and Slack without slowing down (TMS Logging)
TMS Logging is a logging framework for Delphi applications, with outputs to files, the console, databases and chat channels. Sending errors to a Discord or Slack channel through a webhook is a popular way to be warned right away when something breaks in production. The catch: a webhook is a remote HTTP call, and webhooks are rate limited. When a burst of errors came in, every log call ended up waiting for a full webhook round trip, slowing down the very server that was already in trouble.
The Discord and Slack output handlers now deliver messages in the background. A log call only adds the message to an in-memory queue and returns, and a background thread sends the queued messages in batches, honoring HTTP 429 rate limits and retrying failed requests. Registration is the same as before, and the new properties let you tune the delivery:
uses
VCL.TMSLogging, TMSLoggingCore, TMSLoggingDiscordOutputHandler;
procedure SetupDiscordLogging;
var
Discord: TTMSLoggerDiscordWebhookOutputHandler;
begin
Discord := TMSLogger.RegisterOutputHandlerClass(TTMSLoggerDiscordWebhookOutputHandler,
['https://discord.com/api/webhooks/0000000000/abcdefgh']) as TTMSLoggerDiscordWebhookOutputHandler;
Discord.MaxQueueSize := 5000; // messages waiting for delivery
Discord.BatchDelay := 1000; // milliseconds to gather a burst into fewer requests
end;
If messages come faster than they can be delivered and the queue fills up, new ones are dropped instead of slowing the application down, and a message saying how many lines were lost is posted to the channel. The machinery is available as the new TTMSLoggerAsyncOutputHandler base class, a good starting point for your own handlers targeting a web service. See Webhook delivery for all the properties.
12. ZeosLib 8 support (TMS Aurelius)
Aurelius connects to databases through adapters for many data-access libraries, including ZeosLib. ZeosLib 8 changed the type of its Params property, which broke the Aurelius driver. The driver now works with both ZeosLib 7 and ZeosLib 8 (Delphi 10.3 Rio and up), so you can upgrade ZeosLib without touching your Aurelius code.
13. Correct if..else in generated code (TMS BIZ Core Library)
Besides configuration and cancellation tokens, the BCL includes a Delphi code generator: you build a code model and TDelphiCodeGenerator turns it into formatted source code.
This release fixes a classic dangling else problem. When the true branch of an if statement was a single statement that could swallow the else clause, such as another if without else, the generated code compiled but meant something different:
// Before: the else binds to the inner if
if A then
if B then
DoSomething
else
DoOther;
Such branches are now wrapped in begin..end, so the generated code does what the model says:
// After
if A then
begin
if B then
DoSomething;
end
else
DoOther;
14. Smaller fixes
- Delphi 13.2: Aurelius and Sparkle packages failed to compile in Delphi 13.2, due to a regression in the Delphi compiler. They compile again.
- Hosted console servers on Windows: clicking the close button of the console window made the server hang for a few seconds before stopping. It now stops right away.
Summary
| Product | Version | Highlights |
|---|---|---|
| TMS Aurelius | 5.31 | Tenant catalog and database sharding, ZeosLib 8 support, Delphi 13.2 fix |
| TMS Sparkle | 3.41 | Sparkle.Host hosting layer, configuration for hosted servers, TJobWorker background jobs, JWT with OpenSSL 3, HTTP/2 fix for http.sys, TMS Sparkle Server wizard, TSparkleDispatcher.Servers |
| TMS XData | 5.31 | TXDataTenantConnectionPool, TMS XData Server wizard, inherited service contracts, API reference |
| TMS Sphinx | 2.6 | One user per external identity, client_secret_post for upstream providers, expires_in as string, logout and Firebird fixes |
| TMS RemoteDB | 2.27 | TMS RemoteDB Server wizard |
| TMS BCL | 2.6 | Application configuration, cancellation tokens, JWT with OpenSSL 3, JSON Web Keys, PSS signing, JWT security fixes, dangling else fix |
| TMS Logging | 2.16 | Non-blocking Discord and Slack output, TTMSLoggerAsyncOutputHandler |
Getting the update
All of these libraries ship together as part of TMS BIZ. Existing customers can update through their usual channel; if you're evaluating, download the trial from the TMS Software website.
The quickest way to get it is Smart Setup, the free package manager for TMS products, which downloads and builds everything, dependencies included:
tms install tms.biz.*
For the full details, see the release notes of each product:
- TMS Aurelius what's new
- TMS Sparkle what's new
- TMS XData what's new
- TMS Sphinx release notes
- TMS RemoteDB what's new
- TMS BCL release notes
- TMS Logging what's new
And don't miss the three deep dives: Multi-Tenancy and Database Sharding in TMS XData, Server Hosting in TMS Sparkle: Console, Windows Service or Linux Daemon and One Build, Every Environment: Configuration in TMS BIZ.
Wagner Landgraf
This blog post has not received any comments yet. Add a comment.
All Blog Posts | Next Post | Previous Post