For years, PostgreSQL has been a quiet powerhouse in the open‑source database world — robust, fast, and enterprise‑ready. It offers performance advantages over SQL Server in many scenarios, especially when indexes aren’t perfectly optimized, and it consumes significantly less RAM. Compared to SQLite, PostgreSQL is far better suited for production workloads and large‑scale installations.
Yet despite these strengths, Umbraco has historically supported only SQL Server and SQLite. That gap eventually led me down a long, fascinating path: building a full PostgreSQL database provider for Umbraco.
This article tells that story — why I started, what I discovered inside Umbraco’s persistence layer, the architectural changes required, and what it means for the future.
Why I Started This Project
My first exposure to PostgreSQL came years ago when a colleague benchmarked it against SQL Server and found it consistently faster. That result stuck with me. I kept wondering why Umbraco didn’t support PostgreSQL, especially when other CMS platforms like TYPO3 had embraced it.
Fast‑forward to last year. During a difficult burnout phase, I noticed how rare Umbraco projects had become in Germany and how dependent Europe remains on U.S. software vendors. With some time to reflect, I dug into Umbraco’s source code — and discovered something surprising:
- NPoco already supports PostgreSQL.
- Umbraco has an abstraction layer (ISqlSyntaxProvider).
That was enough to spark a deep dive. I wanted to see how far I could get.
Very quickly, I found the core issue: Umbraco didn’t consistently use the SQL abstraction layer. Many raw SQL statements bypassed NPoco’s PostgreSQL‑aware mechanisms, causing problems — especially around case sensitivity.
Fortunately, most required changes were isolated to the Umbraco.Infrastructure persistence layer. I submitted several PRs, all reviewed by Andy Butland, and the work gradually took shape.
My PostgreSQL provider will ship as a package when Umbraco 17.3.0 releases on April 2nd, 2026, and I plan to maintain it until Umbraco eventually replaces NPoco with EF Core.
Key Changes Required for PostgreSQL Support
1. Case Sensitivity Fixes
PostgreSQL is case‑sensitive in ways SQL Server is not. Raw SQL like:
var sql = "DELETE FROM umbracoContentVersionCleanupPolicy WHERE contentTypeId = @id";
had to be rewritten using NPoco’s quoting helpers:
var sql = $"DELETE FROM {QuoteTableName(ContentVersionCleanupPolicyDto.TableName)}
WHERE {QuoteColumnName("contentTypeId")} = @id";
This ensures identifiers are correctly quoted for the active database provider.
2. Replacing Raw SQL With Strongly Typed NPoco Methods
NPoco provides generic, type‑safe methods that automatically generate database‑specific SQL. For example:
.Select("sortOrder")
became:
.Select(c => c.SortOrder)
A more complete transformation:
Before:
var result = _scopeAccessor.AmbientScope.Database
.ExecuteScalar("SELECT COUNT(*) FROM umbracoUser");
After:
var db = _scopeAccessor.AmbientScope.Database;
var sql = db.SqlContext
.Sql()
.SelectCount()
.From<UserDto>();
var result = db.ExecuteScalar<int>(sql);
This ensures SQL generation is consistent across providers.
3. Fixing ExecuteScalar<Guid?> Behavior
NPoco’s PostgreSQL implementation requires nullable GUIDs to pass through the full mapping pipeline. That means replacing ExecuteScalar<Guid> with FirstOrDefault<Guid>.
Correct approach:
var sql = sqlContext.Sql()
.Select<NodeDto>(c => c.UniqueId)
.From<NodeDto>()
.Where<NodeDto>(n => n.NodeId == id);
return database?.FirstOrDefault<Guid?>(sql);
This avoids provider‑specific failures.
4. New SQL Extensions: SelectMax
To replace raw MAX() queries, I added two new extension methods:
SelectMax<TDto>(Expression<Func<TDto, object?>> field) SelectMax<TDto>(Expression<Func<TDto, object?>> field, int coalesceValue)
This allowed transformations like:
Before:
SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType
After:
sql = Sql()
.SelectMax<NodeDto>(c => c.SortOrder, 0)
.From<NodeDto>()
.Where<NodeDto>(x =>
x.ParentId == entity.ParentId &&
x.NodeObjectType == NodeObjectTypeId);
var maxSortOrder = Database.ExecuteScalar<int>(sql);
Cleaner, safer, and provider‑agnostic.
5. Extended DTOs
The DTOs are the perfect place to control table and column names. Actually they to did this already, but for the consistent use in hard code strings like raw SQL statements it is quite useful to expose these as constants of the DTO. For example you could use ContentVersionCleanupPolicyDto.TableName instead of "umbracoContentVersionCleanupPolicy".
This is especially useful when you mix it with typed queries like Sql().Select<ContentVersionCleanupPolicyDto>().
Compatibility With Commercial Packages
So far, I’ve tested only Umbraco Forms, and several issues surfaced. Because the package is closed‑source, I couldn’t fix them directly. I’ve implemented workarounds for most features, but a few roadblocks remain.
If enough interest grows, perhaps Umbraco HQ will support deeper integration. Until then, contributions are welcome.
Closing Thoughts
This project began as a curiosity—an experiment during a difficult period. It grew into something meaningful: a full PostgreSQL provider for Umbraco, built on open‑source principles and a desire for more European technological independence.
The work is far from finished, but it’s a solid foundation. And with Umbraco 17.3.0, PostgreSQL support becomes a real, installable option for the community.
If you’re interested in contributing, testing, or exploring the provider, I’d love to collaborate.
You'll find the package on NuGet: Our.Umbraco.PostgreSql.