Workload-Instance Bucket Architecture
This page defines how DTC provisions cloud object storage for backup workloads, and DTC-built applications, that handle multi-tenancy at the application layer. Companion to Cloud Backup Architecture Standards, which covers the per-(client, location, workload) pattern where DTC enforces tenant boundaries via bucket scoping. This page covers the alternative pattern: the application itself owns the multi-tenant boundary, and DTC provisions exactly one bucket per workload instance.
The canonical example is MSP360, which is multi-tenant by design and maintains its own client-to-folder mapping internally. The same pattern applies to internally developed DTC applications that need an object storage bucket and manage their own per-client data segregation.
When to Use This vs. the Per-(Client, Location, Workload) Pattern
| Question | Mode A — Per-(client, location, workload) | Mode B — Workload-instance (this page) |
|---|---|---|
| Who enforces tenant boundaries? | DTC, by giving each tenant its own bucket | The application, by maintaining its own internal client mapping |
| Bucket scope | One client, one location, one workload | One workload instance, many clients |
| Examples | Veeam BDR, TrueNAS Cloud Sync | MSP360, DTC-built multi-tenant services |
| Tenants per bucket | Exactly 1 (one client, one location) | Up to 100 clients (operational cap, see below) |
If the workload is single-tenant by assumption (its UI shows "the data in this bucket" without any client picker), use Mode A. DTC enforces the boundary because the app cannot. If the workload is built to manage multiple clients itself and has a tenant-aware UI and credential model, use Mode B.
When in doubt, default to Mode A. Tighter scoping is always safer.
Core Principles
1. Bucket per workload instance
Each instance of a Mode B workload gets exactly one bucket. A "workload instance" is a specific deployment of a workload type. The DTC MSP360 production deployment is one workload instance; a hypothetical second MSP360 deployment for a different operating context would be another. A specific deployment of a DTC-built multi-tenant service is also a workload instance.
Workload instances are first-class entities. Each carries its own UUIDv7 OUID, distinct from the workload type (which is still a controlled vocabulary token). The instance OUID is what scopes the bucket, the credential, and the lifecycle.
2. The application owns the client boundary
In Mode A, DTC's bucket scoping is the safety mechanism. In Mode B, that responsibility moves to the application. This is a deliberate trade-off and is only acceptable when:
- The application has a tenant-aware UI that does not expose other tenants' data to a technician working in one tenant's context.
- The application maintains its own internal client-to-data mapping that is verifiable and auditable.
- The application's credential model does not let one tenant's credentials reach another tenant's data within the bucket.
If any of these are not true, Mode A is the correct architecture and the workload should not be onboarded to Mode B.
3. HaloPSA is the source of truth for workload-instance context
Workload instances are tracked in HaloPSA via a custom field on a workload-instance asset record. The OUID, bucket name, current client count, owning team, and 1Password vault reference all live there. Provisioning scripts and operational tooling resolve workload-instance context by querying HaloPSA at runtime.
This is the workload-instance equivalent of NinjaOne being the source of truth for org/location/device context in Mode A. Different platform, same pattern: identity is tracked authoritatively, everything else is derived at runtime.
Status note: the HaloPSA custom-field schema for workload instances is not yet defined. Until it is, workload-instance OUIDs and metadata are tracked in 1Password alongside the bucket credential entry as an interim arrangement. Migrate to HaloPSA when the schema lands; do not invent a parallel tracking system in the meantime.
4. Scoped credentials, vault-stored
Same model as Mode A. Each bucket gets its own scoped B2 application key, restricted to that bucket only, stored in 1Password under the workload-instance entry. Never on a developer's laptop, never in git, never pasted into a ticket. The application reads the credential from 1Password (or its operational secret store, sourced from 1Password) at runtime.
Naming Scheme
Bucket name
{workload-token}-{workload-instance-ouid-flat}
| Component | Format | Source |
|---|---|---|
workload-token |
Lowercase token, controlled vocabulary | Per-workload constant (see table below) |
workload-instance-ouid-flat |
32 hex chars, lowercase, no dashes | HaloPSA workload-instance custom field, flattened |
Mode B workload tokens (controlled vocabulary):
| Workload | Token | Tenancy model |
|---|---|---|
| MSP360 Managed Backup | msp360 |
App-managed, multi-tenant |
| Future DTC apps | TBD | App-managed, multi-tenant |
Workload tokens are global across both Mode A and Mode B. A given token belongs to exactly one mode (a workload type is either DTC-tenant-scoped or app-tenant-scoped, not both). Mode A tokens are listed on Cloud Backup Architecture Standards. Mode B tokens are listed above.
Keep tokens short, singular, and hyphen-free. The 50-char B2 bucket-name limit is the hard constraint. With the workload-token + dash + 32-char flat OUID, a 17-char token is the maximum.
Example bucket name:
msp360-018f6b1d7c4a7def8a1b3c4d5e6f7a8b
39 characters, well within the B2 limit.
Folder (object prefix inside the bucket)
{workload-token}-{workload-instance-ouid-dashed}/{app-managed-prefix}/
The top-level folder mirrors the bucket name in dashed UUID form. Same content as the bucket name, but in the canonical dashed format that matches how the OUID is stored in HaloPSA, log lines, and any other system holding the OUID. This makes:
- A full S3 path self-identifying without needing to remember where the bucket name ends and the key begins.
- A future "split this bucket into two" migration trivial. Folders move between buckets without renaming because each folder already encodes its own workload-instance identity.
Below the top-level folder, the application owns the prefix structure entirely. DTC does not dictate or audit the internal layout. MSP360 organizes its data however MSP360 organizes its data; a DTC-built app organizes its data however its schema demands.
Full S3 path example:
msp360-018f6b1d7c4a7def8a1b3c4d5e6f7a8b/msp360-018f6b1d-7c4a-7def-8a1b-3c4d5e6f7a8b/<app-internal-structure>/
The 100-Client Cap
Each Mode B bucket is limited to 100 clients. This is an operational cap, not a B2-enforced limit. It is tracked on the workload-instance HaloPSA record and reviewed before adding a new client to a workload instance.
Why 100
The cap is a migration-safety floor. Both MSP360 and any future DTC-built workload may eventually need to split a single bucket into multiple buckets, for cost reasons, performance reasons, or because an operating boundary changed. That split is implemented as "copy the new clients' folders to a new bucket, then point those clients at the new bucket." The cost and complexity of that operation scales with the object count in the bucket.
100 clients is a count where the resulting object count, even for very active backup workloads, stays in a range where a bucket-to-bucket migration completes in hours rather than days, and where operator-led validation of "did everything copy correctly" remains tractable. It is a deliberately conservative ceiling chosen for the case where DTC operators are doing the migration manually, with no application-level support for cross-bucket sharding.
Lifting the cap
The cap exists only as long as DTC operators are the ones managing scaling. Once either of the following becomes true, the cap can be revisited:
- The vendor's code natively shards across buckets. When MSP360 (or a successor) supports "this storage backend is actually N buckets, route writes to the right one based on tenant," the storage scaling becomes the vendor's problem and the operator-led migration scenario goes away.
- DTC code natively shards across buckets. When a DTC-built workload manages its own bucket pool and routes per-tenant writes to the right bucket, the same logic applies.
When the cap is lifted or changed for a workload, update the workload's row in the table above and document the rationale in the workload-instance HaloPSA record.
Enforcement
- The provisioning workflow for adding a new client to a Mode B workload checks the workload-instance client count against the cap and refuses to add the 101st client.
- Approaching the cap (≥80 clients) generates a HaloPSA ticket to plan the next workload instance and bucket.
- A new workload instance is provisioned by generating a new UUIDv7, creating a new HaloPSA workload-instance record, and running the standard bucket-provisioning workflow against the new OUID.
Bucket Baseline Settings
Every Mode B bucket is provisioned with these settings. Identical to Mode A unless noted.
| Setting | Value | Reason |
|---|---|---|
| Object Lock | Enabled (compliance mode) | Ransomware, insider threat, accidental deletion |
| Default retention period | 14 days (minimum) | Baseline floor; per-workload retention can be longer via individual object stamps |
| Server-side encryption | SSE-B2 (AES-256) enabled | Data-at-rest encryption managed by Backblaze |
| Versioning | Default on (implicit with object lock) | Required for object lock |
| Lifecycle rule | Per-workload (see below) | The application's retention model determines hidden-version cleanup |
| Bucket visibility | allPrivate | Backup data is never public |
| CORS | Not configured | No browser access required |
Lifecycle rule details
Mode B applications generally manage retention themselves at the application layer, the same way Veeam does in Mode A. The B2 lifecycle rule is therefore a "delete hidden versions ASAP" policy by default, deferring retention decisions to the app.
Per-workload policy:
| Workload token | Hidden-version retention | Rationale |
|---|---|---|
msp360 |
Delete immediately — daysFromHidingToDeleting: 1 (B2 minimum) |
MSP360 manages its own retention and version history at the application layer. A hidden version at the B2 layer is already stale. |
If a Mode B workload is later identified as one that does not manage its own version history, it gets a longer hidden-version retention window the same way TrueNAS does in Mode A. Document the change here at the same time the workload's lifecycle rule is updated.
Example rule (MSP360 bucket):
[{
"fileNamePrefix": "",
"daysFromUploadingToHiding": null,
"daysFromHidingToDeleting": 1
}]
Compliance-mode object-lock interaction: identical to Mode A. Lifecycle deletion is a no-op against objects still under lock. Effective delete window is max(rule, remaining_lock_time).
Credential Management
Same model as Mode A.
| Property | Value |
|---|---|
| Capabilities | listBuckets, readBuckets, writeBuckets, deleteBuckets, listFiles, readFiles, writeFiles, deleteFiles, readBucketEncryption, writeBucketEncryption, readBucketRetentions, writeBucketRetentions, readFileLegalHolds, writeFileLegalHolds, readFileRetentions, writeFileRetentions, bypassGovernance |
| Bucket restriction | Restricted to the provisioned bucket only |
| Name prefix | Not restricted (full bucket access, but only to this bucket) |
| Duration | Unlimited (no expiration) |
Credential storage:
- Primary: 1Password, under the workload-instance entry, tagged with the bucket name and workload token.
- HaloPSA: a reference field on the workload-instance record pointing at the 1Password entry. Never the raw credential.
- Application-side: read at runtime from 1Password or whatever operational secret store the application uses, sourced from 1Password.
Blast radius: a compromised key reaches the entire bucket, and therefore all clients on that workload instance (up to 100). This is a strictly larger blast radius than Mode A, and is one of the reasons Mode B is only chosen when the application's tenant-aware credential model justifies it.
Rotation cadence and triggers are the same as Mode A:
- Key suspected of compromise
- Technician with access departs
- Annual scheduled rotation (tracked as a recurring HaloPSA ticket)
Workload-Instance Lifecycle
Provisioning a new workload instance
- Generate a UUIDv7 (see Operational UUID (OUID) Standard for generation commands).
- Create the workload-instance record in HaloPSA. Populate the OUID, owning team, intended workload token, and an empty initial client roster. (Until the HaloPSA schema exists, capture the same fields in 1Password.)
- Run the bucket-provisioning workflow: create the bucket with the computed name, apply baseline settings, generate the scoped application key, store the key in 1Password under the workload-instance entry.
- Configure the application with the scoped credential and bucket name.
- Begin onboarding clients to the workload instance via the application's own onboarding flow. Track each addition on the HaloPSA record.
Splitting a workload instance (cap reached)
- Generate a new UUIDv7 and provision a new workload instance per the steps above.
- Identify which clients move to the new instance. The simplest split is "new clients go to the new instance." If a backwards split is required, plan the data migration explicitly.
- For any clients moving from the old instance to the new instance, copy their app-managed folder tree from the old bucket to the new bucket.
- Update the application's client-to-instance mapping.
- Once all migrating clients are confirmed live on the new instance and the old folders have aged past their object-lock retention floor, delete the old folders.
Decommissioning a workload instance
- Confirm zero clients remain on the workload instance (check the HaloPSA record).
- Wait for object lock retention to expire on the last objects in the bucket.
- Delete the bucket via
b2_delete_bucket. - Archive the HaloPSA workload-instance record. Do not hard-delete; it is the historical reference for the OUID.
- Revoke and remove the 1Password credential entry.
Why This Architecture
The short version, for the next tech or AI that reads this and wonders why it's not just Mode A:
- One bucket per workload instance: because the application is multi-tenant by design and managing 100+ separate buckets per workload is operational overhead with no safety benefit. The application already has a tenant model. DTC piggybacks on it instead of duplicating it.
- Workload-instance OUID as the bucket key: because the workload instance is the natural unit of isolation, lifecycle, and credential scope in this mode. It is what fails together, scales together, and gets decommissioned together.
- HaloPSA as source of truth: because workload instances are operational concerns more than asset concerns, and HaloPSA is the platform where operational state already lives. NinjaOne is the right home for physical assets; HaloPSA is the right home for workload instances.
- 100-client soft cap: because the absence of cross-bucket sharding in current vendor and DTC code means a future split is operator-led, and operator-led splits stay tractable below this object-count band. Lift the cap when sharding is automated.
- App-managed folder structure below the workload-instance prefix: because dictating internal layout fights the application instead of supporting it. The workload-instance prefix gives DTC the grep-ability it needs at the boundary; everything below is the app's domain.
- Compliance-mode object lock and SSE-B2: same threat model as Mode A. The tenant-boundary trade-off is the only thing that changes between modes. Data-at-rest and immutability requirements do not.
Every piece of this architecture is answering a specific failure mode. When changing any of it, ask: what failure mode was this defending against, and is the change still safe against that failure?
Related
- Cloud Backup Architecture Standards — the per-(client, location, workload) pattern (Mode A)
- Operational UUID (OUID) Standard — UUIDv7 generation, what carries an OUID, source-of-truth platforms
- Backup & Data Protection Standards — RPO/RTO targets, retention philosophy, testing requirements
- Client Credential Administration Standard — credential scoping and rotation cadence