Database, in depth

An enterprise-grade relational data model. Designed by a human.

300+ tables, 700+ stored procedures, and 40+ generic entity types — the shapes you need for users, accounts, content, taxonomy, billing, audit, and translation. Architected by someone who has consolidated databases for the Fortune 500, not stitched together from migration files.

Scale & provenance

The shape of an enterprise schema, before you write a feature.

Numbers first, then the architectural decisions that produced them. Both matter equally — the headcount on the schema tells you what to expect; the design tells you why it scales.

Scale by the numbers

300+ tables, 700+ stored procedures, 40+ entity types, 100+ supported languages. The shape of an enterprise schema before you write your first feature.

300+
tables across users, accounts, roles, permissions, entities, translations, media, payments, notifications, support, tasks, AI
700+
stored procedures — optimized, parameterized, instrumented
40+
generic entity types for central management of operational and historical data
100+
languages supported by the dynamic translation system — no schema changes required

Designed by an architect, not generated.

Every table, every relationship, every index was decided by someone who has consolidated databases for the Fortune 500. Not ORM-generated migrations stitched into a pile — these are hand-tuned T-SQL schemas and 700+ stored procedures, accessed via Dapper + ADO.NET, no EF. Four foundational decisions baked in on day one:

Two-layer caching

Redis plus .NET in-memory caching layered against the schema for both hot reads and distributed coherence. Cache invalidation derived from the data, not invented per feature.

Versioning as a primitive

Agreements, prompts, notify templates, artifact templates — all keep full version history with restore. Soft-delete and entity-suspend are first-class.

Generic entity model

40+ entity types share one base table, indexed for the patterns they actually run. New content types become row inserts — no migration required.

Row-level access at the schema

The role and permission framework runs inside the stored procs, not just the API. Roles decide which rows a user can see, modify, or aggregate.

Relational design

Tables, keys, and the relationships between them.

The shape that makes everything else possible. Primary keys are stable; foreign keys make ownership explicit; junction tables turn intent into structure. The cost of getting this right once — and never having to re-litigate it.

PK and FK as a primitive.

A typical RBAC slice. Every table has its own primary key; foreign keys spell out exactly who owns what; the junction table turns a many-to-many into something the query planner can index.

Accountstenant root
  • idintPK
  • namevarchar(128)
  • planvarchar(32)
  • createdAtdatetime2
Userstenant-scoped
  • idintPK
  • emailvarchar(256)
  • namevarchar(128)
  • accountIdintFK → Accounts
Rolestenant-scoped
  • idintPK
  • codevarchar(64)
  • labelvarchar(128)
  • accountIdintFK → Accounts
UserRolesjunction (M:N)
  • userIdintPK · FK → Users
  • roleIdintPK · FK → Roles
  • assignedAtdatetime2
  • assignedByintFK → Users

Color matches the badge to the table it points at. Follow the emerald, you trace ownership through Users; follow the amber, you trace authorization through Roles.

Excerpt from the live schema.

A real slice of the production data model — entities, ownership, junctions, audit columns, all visible at a glance. Tables don’t lie, and neither do the relationships between them.

Excerpt of the CleenUI data model — a slice of related tables, foreign-key relationships, and audit columns from the production schema.
Engine choices

Stored procs and Dapper. Performance is structural.

Performance is a property of the schema, not a bolt-on layer above it. Stored procedures handle the data shape; Dapper handles the C# round-trip; nothing in between gets in the way.

Stored procedures, not ORMs.

700+ optimized, parameterized, instrumented stored procs. Every endpoint calls one. The query planner sees stable shapes; the indexes are tuned to match.

-- Account-scoped users with role filter (one of 700+ procs)
EXEC dbo.usp_Users_GetForAccount
  @accountId  = @currentAccountId,
  @roleCode   = 'admin',
  @page       = 1,
  @pageSize   = 50;

-- Parameterized · indexed · instrumented
-- Avg execution: 6.2ms across 12k accounts
-- Logged to JobRun + System Performance dashboards

Dapper data layer, mockable repositories.

Lightweight, fast micro-ORM. Every repository has an interface. Tests don’t need a database; production gets raw SQL performance.

IUserRepository.cs
public interface IUserRepository
{
    Task<User> GetByIdAsync(int id);Task<IEnumerable<User>> GetForAccountAsync(int accountId);Task<int> SaveAsync(User user);
}

// Mockable in tests.
// Every entity has its own IXxxRepository.
UserRepository.cs
public class UserRepository : IUserRepository
{
    private readonly IDbConnection _db;public Task<User> GetByIdAsync(int id) =>
        _db.QuerySingleOrDefaultAsync<User>(
            "dbo.usp_Users_GetById",
            new { id },
            commandType: CommandType.StoredProcedure);
}
Recoverability

Nothing is gone. Everything is recoverable.

Versioning and audit history are first-class primitives, not bolted on later. When the auditor asks “what did this look like in March?” the answer is in the schema.

Versioning and restore as a primitive.

Agreements, prompts, notify templates, artifact templates — full version history with one-click restore. Soft-delete and entity-suspend are first-class.

Agreements
Terms, privacy policies, and other legal text. Every save creates a new version; restore any prior one.
v1v2v3v4restorable
Prompts (cleenLLM)
Versioned LLM prompt templates with restore-on-write. Tweak a prompt, ship, roll back if quality dips.
v1v2v3v4v5restorable
Notify templates
Email, SMS, and push templates with reusable translated phrases. Test fire, restore, and dependency mapping built in.
v1v2v3restorable
Artifact templates
Templated structured documents — headings, tasks, questions. Clone, version, restore at the template level.
v1v2v3v4restorable

Audit and history tables, built in.

Every operationally interesting record has its history table from day one. Auditors don’t ask “where’s the trail?” — they read it.

Userslive
  • id int (PK)
  • email varchar(256)
  • name varchar(128)
  • accountId int (FK)
  • updatedAt datetime2
UsersHistoryaudit
  • id int
  • email varchar(256)
  • name varchar(128)
  • accountId int
  • updatedAt datetime2
  • validFrom datetime2
  • validTo datetime2
  • changedBy int (FK Users)
  • changeReason varchar(256)
Generic primitives

Designed for the questions you haven’t thought of yet.

Translation, content typing, access control, taxonomy — generic primitives that any new feature opts into. New languages, new entity types, new roles, new categories all happen as data, not code.

Dynamic table-level translations, no schema changes.

Register any table + column for translation through the registry. Add a new language, run the pending-translation queue, ship. No DDL involved.

Step 1 · Register
RegisterTranslation(
  'Categories',
  'Name'
)
Any table + column pair, registered once.
Step 2 · Queue
[ pending‑translation queue ]
cleenTranslationGenericFunction picks it up; Azure AI Translator and LLM fallbacks resolve.
Step 3 · Cached
[ cached for 100+ languages ]
Redis-backed; served on the next API call.

40+ entity types, generic by design.

One central entity model handles every operationally interesting thing on the platform. Adding a new content type is a row insert, not a migration.

AccountUserRolePermissionCategoryTopicTranslationMediaNotificationTemplateFilterEditionAgreementProjectTaskArtifactHelpArticleAssessmentWordRuleFlagPromptAgentAvatarNewsEvent+ 16 more
One base entity table, 40+ specializations. New content types are configured, not coded.

Row-level access control at the database layer.

The role and permission framework runs at the schema, not just the API. Roles decide which rows a user can see, modify, or aggregate — enforced inside the stored procs themselves.

-- Inside dbo.usp_Entities_Search
SELECT e.*
FROM Entities e
WHERE e.AccountId IN (    SELECT au.AccountId    FROM AccountUsers au    WHERE au.UserId = @currentUserId      AND au.IsActive = 1);

-- The same filter pattern lives in every cross-account proc.
-- Row visibility is enforced where the data is, not in the API.

Hierarchical category and topic taxonomies.

Parent/child trees, pending-review pipelines, replace-and-delete merge handling. A taxonomy that scales without becoming spaghetti.

Industries ├─ Software & Technology │ ├─ SaaS │ ├─ Developer Tools │ └─ AI / ML ├─ Financial Services │ ├─ Banking │ └─ Insurance └─ Healthcare ├─ Hospitals └─ Telehealth
merge & replaceOne stored proc atomically rewrites references on the merged-from branch to the merged-to. Pending-review pipeline holds untrusted submissions.

Get a codebase walkthrough zoom call

Book a 30-minute architecture review. We’ll share-screen through the codebase live and show you where your existing platform plugs in.