-
Notifications
You must be signed in to change notification settings - Fork 16
PostgreSQL Schema Management
LiteBus PostgreSQL inbox and outbox stores need a small, stable set of tables and indexes. v5 ships schema helpers and canonical .sql files that let you choose how those objects are created, upgraded, and validated without tying LiteBus to a specific migration framework.
LiteBus uses two independent version numbers. Mixing them up is a common source of confusion.
| Version | Stored where | What it versions | Who changes it |
|---|---|---|---|
| Contract version | Each inbox/outbox row (contract_version) |
JSON payload shape for one message type | Your application when you register Contracts.Register<T>(name, version: 2)
|
| Table schema version |
litebus_schema_versions metadata table |
Physical columns and indexes for the store table | LiteBus when you call EnsureAsync or run published upgrade scripts |
Contract version is per message type. Table schema version is per physical store table.
| Package | Role |
|---|---|
LiteBus.PostgreSql |
Shared SQL templates, identifier quoting, schema version metadata, advisory locks, logging hook, drift exceptions |
LiteBus.Inbox.PostgreSql |
Inbox store, PostgreSqlInboxSchema helper, inbox SQL files |
LiteBus.Outbox.PostgreSql |
Outbox store, PostgreSqlOutboxSchema helper, outbox SQL files |
LiteBus.Inbox.PostgreSql.Extensions.Microsoft.Hosting |
Opt-in hosted schema creation for inbox |
LiteBus.Outbox.PostgreSql.Extensions.Microsoft.Hosting |
Opt-in hosted schema creation for outbox |
LiteBus.PostgreSql depends only on Npgsql.
LiteBus ships the DDL as plain .sql files. The runtime loads them as embedded resources inside the DLL (so EnsureAsync and GetCreateScript work from NuGet). The same files are also included as loose files in the NuGet package under sql/ for copy-paste into migration tools.
After dotnet add package LiteBus.Inbox.PostgreSql, open the package folder in your NuGet cache:
~/.nuget/packages/litebus.inbox.postgresql/{version}/sql/inbox/v1/create.sql
~/.nuget/packages/litebus.inbox.postgresql/{version}/sql/inbox/v1/ensure_indexes.sql
Shared metadata and upgrade scripts ship in LiteBus.PostgreSql:
~/.nuget/packages/litebus.postgresql/{version}/sql/metadata/create.sql
~/.nuget/packages/litebus.postgresql/{version}/sql/shared/add_trace_context_column.sql
On Windows the root is typically %USERPROFILE%\.nuget\packages\.
You can also copy from the LiteBus source tree (same file contents as the NuGet package):
| File | Purpose |
|---|---|
src/LiteBus.PostgreSql/Sql/metadata/create.sql |
Creates litebus_schema_versions
|
src/LiteBus.Inbox.PostgreSql/Sql/inbox/v1/create.sql |
Creates the version 1 inbox table and indexes |
src/LiteBus.Inbox.PostgreSql/Sql/inbox/v1/ensure_indexes.sql |
Re-applies inbox indexes idempotently |
src/LiteBus.Outbox.PostgreSql/Sql/outbox/v1/create.sql |
Creates the version 1 outbox table and indexes |
src/LiteBus.Outbox.PostgreSql/Sql/outbox/v1/ensure_indexes.sql |
Re-applies outbox indexes idempotently |
src/LiteBus.PostgreSql/Sql/shared/add_trace_context_column.sql |
Version 2 upgrade shared by inbox and outbox |
Discover files from code as well:
PostgreSqlSchemaSqlPaths.Files // shared metadata and upgrade SQL
PostgreSqlInboxSchema.SqlFiles // inbox SQL catalog
PostgreSqlOutboxSchema.SqlFiles // outbox SQL catalog
// Repository path constants:
PostgreSqlInboxSchemaSqlPaths.V1Create // src/LiteBus.Inbox.PostgreSql/Sql/inbox/v1/create.sql
PostgreSqlOutboxSchemaSqlPaths.V1Create // src/LiteBus.Outbox.PostgreSql/Sql/outbox/v1/create.sqlWhen object names differ from LiteBus defaults, call GetCreateScript or GetUpgradeScript to render the embedded SQL templates with your options:
var options = new PostgreSqlInboxStoreOptions
{
SchemaName = "app",
TableName = "litebus_inbox_commands"
};
var ddl = PostgreSqlInboxSchema.GetCreateScript(options);
File.WriteAllText("V001__litebus_inbox.sql", ddl);Both approaches are valid. Copying the .sql files gives DBAs full control. Rendering with GetCreateScript avoids manual token replacement.
LiteBus ships SQL as plain files that are compiled into the assembly and also copied into the NuGet package. Follow these practices when you own migrations or call schema helpers in production.
Edit the .sql files under each package's Sql/ folder in the LiteBus repository. Do not maintain a forked copy with different DDL unless your DBAs require it. When you need custom object names, keep the canonical scripts and render them with GetCreateScript / GetUpgradeScript rather than hand-editing placeholders.
| Location | When it is used | Best practice |
|---|---|---|
| Embedded resource in the DLL |
EnsureAsync, GetCreateScript, integration tests |
Trust this at runtime; it always matches the package version you referenced |
Loose sql/ folder in the NuGet package |
DBA review, Flyway/Liquibase copy-paste | Copy verbatim into your migration repo; pin the LiteBus package version in release notes |
GitHub src/.../Sql/ tree |
Documentation and PR review | Same content as the NuGet loose files for a given release tag |
Runtime code never reads loose files from disk. Deployments that omit the sql/ folder from the published app still work because the embedded copy is always present.
Placeholders use the form {{TokenName}}. Prefer GetCreateScript(options) over manual search-and-replace so schema names, table names, and index names stay correctly quoted for PostgreSQL.
| Approach | When to use |
|---|---|
UseDataSource(NpgsqlDataSource) |
Production apps that already build one shared data source for pooling, health checks, and tracing |
UseConnectionString(string) |
Samples, tests, and small services where the module should create and register the data source |
When inbox and outbox share one database, build a single NpgsqlDataSource and pass it to both stores with UseDataSource. Calling UseConnectionString on both stores creates two pools against the same server.
// Preferred when both stores use the same database
var dataSource = NpgsqlDataSource.Create(configuration.GetConnectionString("OrdersDb")!);
liteBus.AddPostgreSqlCommandInboxStore(p => p.UseDataSource(dataSource));
liteBus.AddPostgreSqlOutboxStore(p => p.UseDataSource(dataSource));
// Simpler when only one store is registered
liteBus.AddPostgreSqlCommandInboxStore(p =>
p.UseConnectionString(configuration.GetConnectionString("OrdersDb")!));Record the LiteBus package version and CurrentSchemaVersion in your deployment runbook. After upgrading LiteBus, apply any new upgrade script before rolling out pods that call ValidateAsync with ValidateSchemaCreationOnStartup = true.
If you contribute to LiteBus, keep SQL paths stable. The loader resolves {AssemblyName}.Sql.{path.with.dots}.sql. Renaming folders without updating EmbeddedResource items breaks runtime schema creation.
| Token | Example rendered value | Used in |
|---|---|---|
{{QuotedSchemaName}} |
"app" |
Store table create scripts |
{{QualifiedTableName}} |
"app"."litebus_inbox_commands" |
Store DDL and v2 upgrade |
{{QuotedMetadataSchemaName}} |
"app" |
Metadata table create |
{{QualifiedMetadataTableName}} |
"app"."litebus_schema_versions" |
Metadata DDL |
{{IdempotencyIndexName}} |
quoted index name | Inbox only |
{{LeaseIndexName}} |
quoted index name | Inbox and outbox |
{{TopicIndexName}} |
quoted index name | Outbox only |
Pick one model per environment. You can use different models in development and production.
Your team owns schema timing. Copy the canonical .sql files or rendered output from GetCreateScript / GetUpgradeScript.
Leave EnsureSchemaCreationOnStartup = false (the default). Run validation in deploy checks if you want a fail-fast guard:
await PostgreSqlInboxSchema.ValidateAsync(dataSource, options, cancellationToken);Call EnsureAsync from application startup, a one-shot deploy job, or integration test setup. The call is idempotent.
await PostgreSqlInboxSchema.EnsureAsync(dataSource, options, cancellationToken);
await PostgreSqlOutboxSchema.EnsureAsync(dataSource, options, cancellationToken);Use this for internal services, prototypes, and test environments where a migration pipeline is overhead.
Enable automatic schema creation when the generic host starts. Both the store option and the hosting module must be registered.
builder.Services.AddLiteBus(liteBus =>
{
liteBus.AddPostgreSqlCommandInboxStore(postgres =>
{
postgres.UseDataSource(dataSource);
postgres.EnsureSchemaCreationOnStartup();
});
// Register schema hosting BEFORE processor hosting.
liteBus.AddPostgreSqlCommandInboxSchemaHosting();
liteBus.AddCommandInboxModule(inbox => { /* contracts */ });
liteBus.AddCommandInboxProcessorHosting(host => host.PollInterval = TimeSpan.FromSeconds(1));
});The schema hosted service no-ops when EnsureSchemaCreationOnStartup is false.
Both inbox and outbox share the same option shape through PostgreSqlSchemaStoreOptions.
| Option | Default | Purpose |
|---|---|---|
SchemaName |
public |
PostgreSQL schema for the store table |
TableName |
litebus_inbox_commands / litebus_outbox_messages
|
Store table name |
MetadataSchemaName |
public |
Schema for the version metadata table |
MetadataTableName |
litebus_schema_versions |
Version metadata table name |
EnsureSchemaCreationOnStartup |
false |
Run EnsureAsync when the host starts |
ValidateSchemaCreationOnStartup |
true |
Run ValidateAsync after schema creation on startup |
Logger |
null |
Optional IPostgreSqlSchemaLogger for schema operations |
Example with custom object names and logging:
postgres.UseOptions(new PostgreSqlInboxStoreOptions
{
SchemaName = "messaging",
TableName = "command_inbox",
MetadataSchemaName = "messaging",
MetadataTableName = "litebus_schema_versions",
Logger = new ConsolePostgreSqlSchemaLogger()
});Use the same option instance for the store registration and schema helpers so table names stay aligned.
LiteBus.PostgreSql exposes IPostgreSqlSchemaLogger without taking a dependency on Microsoft logging packages. Assign an implementation through PostgreSqlSchemaStoreOptions.Logger.
Schema operations log at these points:
- Starting and completing
EnsureAsync - Advisory lock acquired or waiting on another session
- Creating version 1 objects
- Applying version upgrades
- Recording metadata version rows
- Validation success or
PostgreSqlSchemaDriftExceptiondetails
Example adapter for local development:
public sealed class ConsolePostgreSqlSchemaLogger : IPostgreSqlSchemaLogger
{
public void Log(PostgreSqlSchemaLogLevel level, string message, Exception? exception = null)
{
Console.WriteLine($"[{level}] {message}");
if (exception is not null)
{
Console.WriteLine(exception);
}
}
}Hosting applications can bridge IPostgreSqlSchemaLogger to ILogger<T> in the hosting package without changing LiteBus.PostgreSql dependencies.
LiteBus records applied table schema versions in litebus_schema_versions:
| Column | Purpose |
|---|---|
component |
inbox or outbox
|
schema_name |
Store table schema |
table_name |
Store table name |
version |
Applied table schema version |
applied_at |
UTC timestamp of the last recorded upgrade |
Primary key: (component, schema_name, table_name).
One metadata table serves all LiteBus store tables in the database. Each inbox or outbox table you configure gets its own row.
| Component | Version | Notes |
|---|---|---|
| Inbox | 2 | v1 initial table; v2 adds nullable trace_context jsonb reserved for future tracing metadata |
| Outbox | 2 | v1 initial table; v2 adds nullable trace_context jsonb reserved for future tracing metadata |
Constants: PostgreSqlInboxSchema.CurrentSchemaVersion and PostgreSqlOutboxSchema.CurrentSchemaVersion.
The store implementations do not read trace_context yet. The column exists so future LiteBus releases can add tracing metadata without another immediate breaking DDL change.
| Method | Purpose |
|---|---|
GetCreateScript(options) |
Full rendered DDL for the current schema version, metadata table, and indexes |
GetUpgradeScript(from, to, options) |
Incremental rendered DDL between two table schema versions |
EnsureAsync(dataSource, options, ct) |
Create, upgrade, and record the current version idempotently |
CreateIfNotExistsAsync(...) |
Alias for EnsureAsync kept for readability in tests |
ValidateAsync(dataSource, options, ct) |
Fail fast when the physical table or metadata does not match the library |
SqlFiles |
Catalog of repository SQL file paths and descriptions |
PostgreSqlSchemaDriftException is thrown by ValidateAsync and by startup validation. It includes:
-
Component(inbox/outbox) -
SchemaName,TableName -
ExpectedVersion,ActualVersion -
Details(missing columns, missing table, version mismatch)
Treat this as a deployment blocker. Do not catch and ignore it in production startup.
EnsureAsync is safe when many pods or services start at the same time.
-
Initial create uses
CREATE ... IF NOT EXISTSfor schemas, tables, and indexes. -
Upgrades use idempotent DDL such as
ADD COLUMN IF NOT EXISTS. - Advisory locks ensure one connection applies upgrades at a time per store table. Other instances wait until the table reaches the expected version or the wait timeout elapses.
Lock key format: litebus:{component}:{schema}:{table}.
Waiting instances poll the metadata table and physical column shape. They do not fail merely because another instance holds the lock.
| Operation | Safe from all pods | Recommended pattern |
|---|---|---|
First-time EnsureAsync
|
Yes | Opt-in host schema creation or startup call |
Version upgrade (EnsureAsync) |
Yes (with advisory lock) | Prefer a deploy hook in large fleets; host schema creation is still safe |
| Destructive DDL (drop/rename/type change) | No | Manual migration only; never auto-run from LiteBus |
ValidateAsync |
Yes | Deploy check or startup when ValidateSchemaCreationOnStartup = true
|
If you created tables with early v5 previews using CreateIfNotExistsAsync before metadata existed, EnsureAsync handles the transition:
- Detects the existing table.
- Infers version 1 from column shape when metadata is missing.
- Applies the 1 → 2 upgrade script.
- Writes metadata at version 2.
If metadata says version 2 but a column was removed manually, EnsureAsync trusts the physical column shape and re-applies missing upgrades.
Schema creation must finish before inbox or outbox processors lease rows. Register modules in this order:
liteBus.AddPostgreSqlCommandInboxStore(/* ... */);
liteBus.AddPostgreSqlCommandInboxSchemaHosting(); // 1. schema creation
liteBus.AddCommandInboxModule(/* ... */);
liteBus.AddCommandInboxProcessorHosting(/* ... */); // 2. processor
liteBus.AddPostgreSqlOutboxStore(/* ... */);
liteBus.AddPostgreSqlOutboxSchemaHosting(); // 1. schema creation
liteBus.AddOutboxModule(/* ... */);
liteBus.AddOutboxProcessorHosting(/* ... */); // 2. processorSchema hosting registers an IHostedService that completes StartAsync before background processor loops begin.
For teams that want automatic DDL without running it from every pod:
var dataSource = NpgsqlDataSource.Create(connectionString);
await PostgreSqlInboxSchema.EnsureAsync(dataSource, inboxOptions);
await PostgreSqlOutboxSchema.EnsureAsync(dataSource, outboxOptions);
await PostgreSqlInboxSchema.ValidateAsync(dataSource, inboxOptions);
await PostgreSqlOutboxSchema.ValidateAsync(dataSource, outboxOptions);
Console.WriteLine("LiteBus PostgreSQL schema is ready.");Run this once per deployment before rolling out application pods with EnsureSchemaCreationOnStartup = false.
- Copy the
.sqlfiles fromPostgreSqlInboxSchema.SqlFiles/PostgreSqlOutboxSchema.SqlFilesinto your migration repository, or generate rendered scripts withGetCreateScript. - On LiteBus upgrades that bump
CurrentSchemaVersion, add the new upgrade.sqlfile or callGetUpgradeScript(previous, current). - Record the LiteBus release and schema version in your internal runbook.
- Call
ValidateAsyncfrom a smoke test after migration.
Prefer copying the shipped .sql files verbatim. Edit only when DBAs require renames, tablespaces, or ownership clauses.
If you implement ICommandInboxWriter / IOutboxMessageWriter and your own storage, you own the schema entirely. LiteBus does not require the metadata table or trace_context column for custom stores. Match the envelope fields your implementation reads and writes.
When LiteBus bumps CurrentSchemaVersion:
- Read the release notes and migration guide entry for the target version.
- Add the new
.sqlupgrade file to your migration pipeline, or applyGetUpgradeScript(oldVersion, newVersion). - Deploy application code that expects the new library version after DDL succeeds.
- Prefer additive, backward-compatible DDL (nullable columns, new indexes). LiteBus auto-upgrade only runs safe scripts.
LiteBus will not auto-run destructive changes (drops, renames, narrowing type changes) from application pods.