Skip to main content

DATABASE DESIGN DOCUMENT

Roundtrip · A Traxs Company Product · Version 1.0 · March 2026

SQL Server schema design, table definitions, indexing strategy, multi-tenancy data isolation, and migration approach for the Roundtrip platform.

AttributeValue
Document TitleRoundtrip Database Design Document
Version1.0 — Initial Release
StatusDraft — In Review
DatabaseSQL Server 2022+
ORMEntity Framework Core 9 + Dapper (reporting queries)
Related DocsDomain Model v1.0, SAD v1.0
DateMarch 2026

01 — DESIGN PRINCIPLES

Design Principles

PrincipleApplied As
Multi-tenancy firstEvery tenant-scoped table has a TenantId (uniqueidentifier NOT NULL) column. EF Core Global Query Filter applies it on every query automatically. SQL Server Row-Level Security provides defence-in-depth.
No hard deletesAll tenant-owned entities use IsDeleted (bit) + DeletedAt (datetime2). Physical deletes never occur on tenant data. System tables may use hard deletes.
Temporal tables for auditServiceTickets, Invoices, InventoryItems, and Clients use SQL Server Temporal Tables. Full row history is maintained automatically with FOR SYSTEM_TIME queries.
Strongly typed GUIDsAll primary keys are uniqueidentifier (GUID). No integer identity keys — GUIDs prevent enumeration attacks and simplify distributed scenarios.
Owned types for value objectsEF Core Owned Entities map C# value objects (ServiceAddress, ContactInfo, Money) to columns in the parent table — no join tables for value objects.
Denormalisation for historyPartUsage stores ItemName and UnitCostAtTime as snapshot copies so historical records remain accurate even if the inventory item changes.
Indexes on every FKEvery foreign key column has a non-clustered index. Query plan analysis reviewed before every migration.
Schema per tier (Professional)Professional tier tenants get a dedicated schema (e.g., tenant_acme.Tickets) within the shared database. EF Core schema name is resolved from TenantContext at runtime.

02 — SCHEMA OVERVIEW

Schema Overview

Database Layout

Shared Database: roundtrip_shared

├── Schema: dbo (system / platform tables)
│ ├── Tenants
│ ├── HangfireJobs (Hangfire schema)
│ └── __EFMigrationsHistory

├── Schema: dbo (Standard tier — row-level TenantId isolation)
│ ├── ServiceTickets ← Temporal Table
| |-- ServiceTicketHistory
│ ├── PartUsages
│ ├── TicketNotes
│ ├── TicketAttachments
│ ├── Clients ← Temporal Table
│ ├── ClientAddresses
| |-- ClientHistory
│ ├── Technicians
│ ├── TenantUsers
| |-- TenantSettings
│ ├── InventoryItems ← Temporal Table
| |-- InventoryItemsHistory
| |-- PartUsages
│ ├── StockMovements
│ ├── Invoices ← Temporal Table
| |-- InvoicesHistory
│ ├── InvoiceLineItems
│ ├── InvoiceDeliveries
│ ├── Quotes
│ ├── QuoteLineItems
│ ├── TechnicianRoutes
│ |── RouteStops
|-- Notifications
|__ ReportJobs


Key Legend

SymbolMeaning
PKPrimary Key — uniqueidentifier, clustered index
FKForeign Key — with non-clustered index on the FK column
NOT NULLColumn is required — cannot be null
NULLColumn is optional — nullable
TEMPORALTable has SQL Server Temporal Table history tracking (SysStartTime, SysEndTime)

03 — PLATFORM TABLES

Platform Tables

Tenants

The master tenant registry. Contains one row per registered business. Consulted by the tenant resolution middleware on every request.

Tenants — dbo schema — one row per registered business

ColumnTypeNullKeyNotes
TenantIduniqueidentifierNOPKNEWID() — primary key, used in all FK references
TenantSlugnvarchar(30)NOURL-safe slug (e.g. acme-hvac). Unique across all tenants. Used in subdomain routing.
CompanyNamenvarchar(200)NODisplay name of the Business
Plannvarchar(20)NOCHECK: Starter | Standard | Professional | Enterprise
Statusnvarchar(20)NOCHECK: Active | Suspended | Cancelled | Trial
IsolationModelnvarchar(20)NOCHECK: SharedSchema | DedicatedSchema | DedicatedDatabase
SchemaNamenvarchar(60)YESNULL for SharedSchema; e.g. tenant_acme for DedicatedSchema
ConnectionStringnvarchar(500)YESNULL except for DedicatedDatabase tenants. Encrypted at application layer.
Timezonenvarchar(60)NOIANA timezone (e.g. America/New_York). Used for scheduled jobs.
DefaultCurrencynchar(3)NOISO 4217 (e.g. USD, CAD, AUD)
LogoBlobKeynvarchar(500)YESBlob storage key for tenant logo
TrialEndsAtdatetime2YESNULL for paid tenants. Trial expiry timestamp.
CreatedAtdatetime2NOUTC timestamp of tenant registration
IsDeletedbitNOSoft delete flag. Default 0.
DeletedAtdatetime2YESUTC timestamp of soft deletion

Indexes: UQ: TenantSlug · IX: Status, Plan

04 — TICKETS TABLES

Tickets Tables

ServiceTickets — TEMPORAL TABLE — core work record

ColumnTypeNullKeyNotes
TicketIduniqueidentifierNOPK
TenantIduniqueidentifierNOFKFK → Tenants.TenantId. Part of composite index with Status.
TicketNumbernvarchar(20)NOFormat RT-YYYY-NNNNN. Unique within tenant.
ClientIduniqueidentifierNOFKFK → Clients.ClientId
ClientAddressIduniqueidentifierNOFKFK → ClientAddresses.ClientAddressId — service location for this ticket
TechnicianIduniqueidentifierYESFKFK → Technicians.TechnicianId. NULL = unassigned.
ServiceDescriptionnvarchar(2000)NOFreetext description of work requested
Prioritynvarchar(20)NOCHECK: Low | Normal | High | Urgent
Statusnvarchar(20)NOCHECK: Open | Assigned | InProgress | OnHold | Completed | Cancelled
OnHoldReasonnvarchar(50)YESNULL unless Status = OnHold. CHECK: AwaitingParts | CustomerNotHome | Rescheduled | AwaitingApproval
RequestedWindowStarttimeYESPreferred arrival window start. NULL = no preference.
RequestedWindowEndtimeYESPreferred arrival window end.
CompletionNotenvarchar(4000)YESRequired when Status = Completed. Min 10 chars enforced at domain layer.
CreatedAtdatetime2NOUTC timestamp of ticket creation
AssignedAtdatetime2YESUTC timestamp of first assignment
StartedAtdatetime2YESUTC timestamp when technician set InProgress
CompletedAtdatetime2YESUTC timestamp of completion
IsDeletedbitNOSoft delete. Default 0.
DeletedAtdatetime2YES
SysStartTimedatetime2NOTEMPORAL: system-managed row validity start
SysEndTimedatetime2NOTEMPORAL: system-managed row validity end

Indexes: UQ: TenantId + TicketNumber · IX: TenantId + Status + CreatedAt DESC (dispatcher list query) · IX: TenantId + TechnicianId + Status (technician dashboard query) · IX: TenantId + ClientId (client history query)

PartUsages — Child records of ServiceTickets

ColumnTypeNullKeyNotes
PartUsageIduniqueidentifierNOPK
TenantIduniqueidentifierNOFKFK → Tenants. Redundant but required for EF Core Global Query Filter.
TicketIduniqueidentifierNOFKFK → ServiceTickets.TicketId
InventoryItemIduniqueidentifierNOFKFK → InventoryItems.InventoryItemId
ItemNamenvarchar(200)NODenormalised snapshot of item name at time of use
UnitCostAmountdecimal(18,4)NOSnapshot of unit cost at time of use (Money VO — Amount)
UnitCostCurrencynchar(3)NOSnapshot of currency at time of use (Money VO — Currency)
QuantityUsedintNOCHECK: > 0
PhotoBlobKeynvarchar(500)YESOptional photo evidence of part usage
RecordedByTechIduniqueidentifierNOFKFK → Technicians.TechnicianId
RecordedAtdatetime2NO

Indexes: IX: TicketId · IX: TenantId + InventoryItemId

TicketNotes — Notes and internal comments on tickets

ColumnTypeNullKeyNotes
TicketNoteIduniqueidentifierNOPK
TenantIduniqueidentifierNOFK
TicketIduniqueidentifierNOFKFK → ServiceTickets
AuthorUserIduniqueidentifierNOKeycloak UserId of the note author
AuthorNamenvarchar(200)NODenormalised display name for history consistency
NoteTextnvarchar(max)NO
IsInternalOnlybitNODefault 0. Internal notes hidden from technician PWA.
CreatedAtdatetime2NO

Indexes: IX: TicketId

TicketAttachments — File attachments linked to tickets

ColumnTypeNullKeyNotes
AttachmentIduniqueidentifierNOPK
TenantIduniqueidentifierNOFK
TicketIduniqueidentifierNOFK
FileNamenvarchar(500)NO
BlobKeynvarchar(500)NOStorage key in blob storage (Azurite / S3-compatible)
ContentTypenvarchar(100)NOMIME type (e.g. image/jpeg, application/pdf)
FileSizeBytesbigintNO
UploadedByUserIduniqueidentifierNO
UploadedAtdatetime2NO

Indexes: IX: TicketId

05 — CLIENTS TABLES

Clients Tables

Clients — TEMPORAL TABLE — business client records

ColumnTypeNullKeyNotes
ClientIduniqueidentifierNOPK
TenantIduniqueidentifierNOFK
CompanyNamenvarchar(200)YESNULL for residential clients
IsCommercialbitNODefault 0. Required with CompanyName.
FirstNamenvarchar(100)NOContactInfo VO — first name
LastNamenvarchar(100)NOContactInfo VO — last name
Emailnvarchar(320)YESContactInfo VO. At least one of Email or PrimaryPhone required (enforced at domain layer).
PrimaryPhonenvarchar(30)YESContactInfo VO
SecondaryPhonenvarchar(30)YESContactInfo VO
Notesnvarchar(max)YESPersistent client-level notes (not ticket notes)
PreferredTechnicianIduniqueidentifierYESFKFK → Technicians. Default assignment suggestion.
IsArchivedbitNOSoft archive. Default 0.
ArchivedAtdatetime2YES
CreatedAtdatetime2NO
SysStartTimedatetime2NOTEMPORAL
SysEndTimedatetime2NOTEMPORAL

Indexes: IX: TenantId + IsArchived · IX: TenantId + Email (duplicate detection)

ClientAddresses — One-to-many service addresses per client

ColumnTypeNullKeyNotes
ClientAddressIduniqueidentifierNOPK
TenantIduniqueidentifierNOFK
ClientIduniqueidentifierNOFKFK → Clients.ClientId
Labelnvarchar(100)NOUser-defined label e.g. 'Main Office', 'Warehouse'
IsPrimarybitNOExactly one per client must be true (enforced at app layer)
Streetnvarchar(300)NOServiceAddress VO
Citynvarchar(100)NOServiceAddress VO
StateProvincenvarchar(100)NOServiceAddress VO
PostalCodenvarchar(20)NOServiceAddress VO
Countrynchar(2)NOServiceAddress VO — ISO 3166-1 alpha-2
Latitudedecimal(9,6)YESPopulated on geocoding. NULL until geocoded.
Longitudedecimal(9,6)YESPopulated on geocoding.
CreatedAtdatetime2NO

Indexes: IX: ClientId · IX: TenantId + PostalCode (proximity queries)

06 — USERS TABLES

Users Tables

Technicians — Field staff — maps to Keycloak user in tenant realm

ColumnTypeNullKeyNotes
TechnicianIduniqueidentifierNOPK
TenantIduniqueidentifierNOFK
KeycloakUserIduniqueidentifierNOKeycloak sub claim. Unique per tenant.
FirstNamenvarchar(100)NO
LastNamenvarchar(100)NO
Emailnvarchar(320)NO
Phonenvarchar(30)YES
Statusnvarchar(20)NOCHECK: Available | EnRoute | OnSite | Unavailable | OffDuty
LastKnownLatdecimal(9,6)YESGpsCoordinate VO — latitude
LastKnownLngdecimal(9,6)YESGpsCoordinate VO — longitude
LocationUpdatedAtdatetime2YES
ProfilePhotoBlobKeynvarchar(500)YES
IsActivebitNODefault 1. Deactivated technicians cannot log in.
CreatedAtdatetime2NO

Indexes: UQ: TenantId + KeycloakUserId · IX: TenantId + IsActive + Status

TechnicianSkills — Skills associated with each technician

ColumnTypeNullKeyNotes
TechnicianSkillIduniqueidentifierNOPK
TechnicianIduniqueidentifierNOFK
TenantIduniqueidentifierNOFK
SkillNamenvarchar(100)NOe.g. HVAC, Plumbing, Electrical, Refrigerant Certified

Indexes: IX: TechnicianId

TenantUsers — Non-technician users: Dispatchers, BillingStaff, Admins

ColumnTypeNullKeyNotes
TenantUserIduniqueidentifierNOPK
TenantIduniqueidentifierNOFK
KeycloakUserIduniqueidentifierNOKeycloak sub claim. Unique per tenant.
FirstNamenvarchar(100)NO
LastNamenvarchar(100)NO
Emailnvarchar(320)NO
Rolenvarchar(30)NOCHECK: TenantAdmin | Dispatcher | BillingStaff | ReadOnly
IsActivebitNODefault 1.
CreatedAtdatetime2NO

Indexes: UQ: TenantId + KeycloakUserId · UQ: TenantId + Email · IX: TenantId + Role

07 — INVENTORY TABLES

Inventory Tables

InventoryItems — TEMPORAL TABLE — parts and consumables catalogue

ColumnTypeNullKeyNotes
InventoryItemIduniqueidentifierNOPK
TenantIduniqueidentifierNOFK
Skunvarchar(50)NOSKU VO. Unique within tenant.
Namenvarchar(200)NO
Descriptionnvarchar(1000)YES
Categorynvarchar(100)YESUser-defined category (Filters, Refrigerant, Electrical, etc.)
UnitOfMeasurenvarchar(30)NOeach | litre | metre | kg | pair
UnitCostAmountdecimal(18,4)NOMoney VO — amount
UnitCostCurrencynchar(3)NOMoney VO — ISO 4217 currency
QuantityOnHandintNOCHECK: >= 0
ReorderThresholdintNOCHECK: >= 0. Alert fires when QuantityOnHand <= this value.
Barcodenvarchar(100)YES
PhotoBlobKeynvarchar(500)YES
IsArchivedbitNODefault 0. Archived items cannot have stock consumed.
ArchivedAtdatetime2YES
CreatedAtdatetime2NO
SysStartTimedatetime2NOTEMPORAL
SysEndTimedatetime2NOTEMPORAL

Indexes: UQ: TenantId + Sku · IX: TenantId + Category · IX: TenantId + QuantityOnHand (low stock query)

StockMovements — Immutable audit log of every stock quantity change

ColumnTypeNullKeyNotes
StockMovementIduniqueidentifierNOPK
TenantIduniqueidentifierNOFK
InventoryItemIduniqueidentifierNOFK
MovementTypenvarchar(20)NOCHECK: Consumption | Adjustment | Receipt
QuantityDeltaintNONegative = removed, positive = added
QuantityAfterintNOSnapshot of QuantityOnHand immediately after this movement
TicketIduniqueidentifierYESFKFK → ServiceTickets. Set for Consumption movements only.
AdjustmentReasonnvarchar(50)YESCHECK (when Type=Adjustment): ReceivedShipment | Damaged | Theft | AuditCorrection | InitialSetup
RecordedByUserIduniqueidentifierNOKeycloak UserId
RecordedAtdatetime2NO

Indexes: IX: InventoryItemId + RecordedAt DESC · IX: TenantId + TicketId

08 — BILLING TABLES

Billing Tables

Invoices — TEMPORAL TABLE — financial billing documents

ColumnTypeNullKeyNotes
InvoiceIduniqueidentifierNOPK
TenantIduniqueidentifierNOFK
InvoiceNumbernvarchar(20)NOFormat INV-YYYY-NNNNN. Unique within tenant.
SourceTicketIduniqueidentifierNOFKFK → ServiceTickets
ClientIduniqueidentifierNOFK
Statusnvarchar(20)NOCHECK: Draft | Sent | Viewed | Paid | Overdue | Cancelled
TaxRatePercentdecimal(5,4)NOTaxRate VO — decimal 0.0000 to 1.0000
SubtotalAmountdecimal(18,4)NOComputed from line items (stored for query performance)
TaxAmountdecimal(18,4)NO
TotalAmountdecimal(18,4)NO
Currencynchar(3)NO
IsLockedbitNODefault 0. Set true on Send. Prevents line item edits.
IssuedAtdatetime2NO
DueDatedatetime2YES
SentAtdatetime2YES
PaidAtdatetime2YES
SysStartTimedatetime2NOTEMPORAL
SysEndTimedatetime2NOTEMPORAL

Indexes: UQ: TenantId + InvoiceNumber · IX: TenantId + Status + IssuedAt DESC · IX: TenantId + ClientId

InvoiceLineItems — Itemized charges on an invoice

ColumnTypeNullKeyNotes
LineItemIduniqueidentifierNOPK
InvoiceIduniqueidentifierNOFK
TenantIduniqueidentifierNOFK
SortOrderintNODisplay order of line items on the invoice
Descriptionnvarchar(500)NO
Quantitydecimal(18,4)NOCHECK: > 0
UnitPriceAmountdecimal(18,4)NO
TotalAmountdecimal(18,4)NOComputed: Quantity × UnitPriceAmount
LineItemTypenvarchar(20)NOCHECK: Labour | Part | Discount | Other

Indexes: IX: InvoiceId

Quotes — Pre-work cost estimates

ColumnTypeNullKeyNotes
QuoteIduniqueidentifierNOPK
TenantIduniqueidentifierNOFK
QuoteNumbernvarchar(20)NOFormat QT-YYYY-NNNNN
ClientIduniqueidentifierNOFK
TicketIduniqueidentifierYESFKOptional link to a ticket
Statusnvarchar(20)NOCHECK: Draft | Sent | Viewed | Approved | Declined | Expired
TotalAmountdecimal(18,4)NO
Currencynchar(3)NO
ExpiresAtdatetime2NO
TermsAndConditionsnvarchar(max)YES
IsConvertedbitNODefault 0. True once converted to Invoice.
ApprovedByNamenvarchar(200)YES
ApprovedAtdatetime2YES
CreatedAtdatetime2NO

Indexes: UQ: TenantId + QuoteNumber · IX: TenantId + Status

09 — ROUTING TABLES

Routing Tables

TechnicianRoutes — One route per technician per day

ColumnTypeNullKeyNotes
TechnicianRouteIduniqueidentifierNOPK
TenantIduniqueidentifierNOFK
TechnicianIduniqueidentifierNOFK
RouteDatedateNOThe calendar date this route covers (DateOnly)
IsOptimizedbitNODefault 0. Set true by OptimizeRoutes job.
OptimizedAtdatetime2YES
VersionintNOIncremented on each re-optimization. Default 1.
CreatedAtdatetime2NO

Indexes: UQ: TenantId + TechnicianId + RouteDate · IX: TenantId + RouteDate

RouteStops — Ordered stops on a technician route

ColumnTypeNullKeyNotes
RouteStopIduniqueidentifierNOPK
TechnicianRouteIduniqueidentifierNOFK
TenantIduniqueidentifierNOFK
TicketIduniqueidentifierNOFKFK → ServiceTickets
StopOrderintNO1-based ordering. CHECK: >= 1
Streetnvarchar(300)NODenormalised ServiceAddress snapshot
Citynvarchar(100)NO
Latitudedecimal(9,6)YES
Longitudedecimal(9,6)YES
EstimatedArrivaldatetime2YES
DrivingMinutesPrevintYESEstimated driving minutes from previous stop
EstimatedDurationMinintYESEstimated job duration in minutes
IsCompletedbitNODefault 0
CompletedAtdatetime2YES
IsPinnedbitNODefault 0. Pinned stops are skipped during re-optimization.

Indexes: IX: TechnicianRouteId + StopOrder · IX: TenantId + TicketId

10 — INDEXING & PERFORMANCE

Indexing Strategy

Query Pattern Analysis

The most frequent and performance-critical queries in Roundtrip are identified below. Each has an index strategy designed to support it.

Query PatternTableIndex UsedNotes
Dispatcher ticket list: all open tickets for tenant, sorted by priority + createdServiceTicketsIX: TenantId + Status + CreatedAt DESCCovering index includes Priority for sort
Technician dashboard: my assigned tickets todayServiceTicketsIX: TenantId + TechnicianId + StatusStatus = Assigned/InProgress only
Client service history: all tickets for a clientServiceTicketsIX: TenantId + ClientIdDate-ordered in application layer
Low stock query: items at or below thresholdInventoryItemsIX: TenantId + QuantityOnHandFiltered index on IsArchived = 0
Inventory lookup by SKU or barcodeInventoryItemsUQ: TenantId + SKU; IX: BarcodeSKU unique index doubles as lookup index
Invoice aging: sent invoices by due dateInvoicesIX: TenantId + Status + DueDateFor overdue calculation job
Route for technician todayTechnicianRoutes + RouteStopsUQ: TenantId+TechId+Date, IX: RouteId+OrderTwo-table join; both indexed
Stock movement history for an itemStockMovementsIX: InventoryItemId + RecordedAt DESCSupports audit log paging

Filtered Indexes

SQL Server filtered indexes are used where queries always include a specific predicate, reducing index size and improving performance:

-- Only active, non-deleted tickets (the vast majority of dispatcher queries)
CREATE NONCLUSTERED INDEX IX_ServiceTickets_Active
ON ServiceTickets (TenantId, Status, CreatedAt DESC)
WHERE IsDeleted = 0;

-- Only non-archived inventory items (normal stock queries)
CREATE NONCLUSTERED INDEX IX_InventoryItems_Active
ON InventoryItems (TenantId, QuantityOnHand)
WHERE IsArchived = 0;

-- Active technicians only (dispatch board query)
CREATE NONCLUSTERED INDEX IX_Technicians_Active
ON Technicians (TenantId, Status)
WHERE IsActive = 1;

Temporal Table Query Examples

-- Who changed this ticket's status and when?
SELECT Status, AssignedAt, TechnicianId, SysStartTime AS ChangedAt
FROM ServiceTickets
FOR SYSTEM_TIME ALL
WHERE TicketId = @ticketId
ORDER BY SysStartTime;

-- What was the inventory level for this item last Tuesday?
SELECT QuantityOnHand, UnitCostAmount
FROM InventoryItems
FOR SYSTEM_TIME AS OF '2026-02-25T00:00:00'
WHERE InventoryItemId = @itemId AND TenantId = @tenantId;

11 — MIGRATION STRATEGY

Migration Strategy

EF Core Migrations

All schema changes are managed through EF Core Migrations. No manual SQL schema changes are made directly to any environment. Every migration is code-reviewed and tested against a Testcontainers SQL Server instance before merging.

# Create a new migration
dotnet ef migrations add AddTechnicianSkillsTable \
--project src/Roundtrip.Infrastructure \
--startup-project src/Roundtrip.Api \
--output-dir Persistence/Migrations

# Apply migrations to dev database
dotnet ef database update \
--project src/Roundtrip.Infrastructure \
--startup-project src/Roundtrip.Api

# Generate SQL script for production (reviewed before applying)
dotnet ef migrations script --idempotent \
--project src/Roundtrip.Infrastructure \
--startup-project src/Roundtrip.Api \
--output migrations.sql

Migration Rules

  • Every migration must be additive or non-destructive in production. Dropping columns uses a two-phase approach: mark nullable, deploy, then drop in next release.
  • Renaming columns is always done as add-new + migrate-data + drop-old across multiple releases, never in a single migration.
  • All migrations run inside a transaction. If any step fails, the migration rolls back completely.
  • The idempotent migration script is generated for every release and reviewed by a second developer before production deployment.
  • Temporal table modifications require disabling the system-time period, altering, then re-enabling — this is handled in the migration builder with raw SQL.

Tenant Database Provisioning

When a new Enterprise tier tenant is onboarded, their dedicated database is provisioned by the TenantOnboardingJob in Hangfire:

// TenantOnboardingJob — step 3: provision database
public async Task ProvisionDatabaseAsync(TenantId tenantId)
{
var dbName = $"roundtrip_{tenantSlug}";
await _masterDb.ExecuteAsync(
$"CREATE DATABASE [{dbName}] COLLATE SQL_Latin1_General_CP1_CI_AS");

var tenantConnStr = BuildConnectionString(dbName);

// Apply all migrations to the new tenant database
var tenantDb = new ApplicationDbContext(tenantConnStr);
await tenantDb.Database.MigrateAsync();

// Store encrypted connection string in Tenants table
await _tenantRegistry.SetConnectionStringAsync(tenantId,
_encryptor.Encrypt(tenantConnStr));
}

Roundtrip · Database Design Document v1.0 · A Traxs Company Product · March 2026