SGSanele GumedeBI · Data · AI

Explore

Navigate the portfolio by evidence and context.

HomePortfolio overview and current workExperienceRoles, scope, and measurable impactProjectsSelected systems and case studiesSkillsBI, data, engineering, and AI capabilitiesAboutBackground, interests, and working style
Contact
Back to projects

Building the operating platform behind a growing digital group.

SGDataInt Group is being built as one governed platform rather than a portfolio of separate websites — a shared foundation where brands, business units and client products run independently on common strategy, governance, data, intelligence and agent capabilities.

I defined the operating model, designed the tenant boundary and the authority model that enforces it, and built the platform foundation the products now run on. The phases still marked planned are design work, not delivered software, and are described that way throughout.

Read the architecture
Role
Founder / Architect / Product & Engineering Lead
Status
In progress
Delivery model
Product-led, multi-tenant, configurable
Disciplines
Business architecture · Platform engineering · Data · AI and agents · Governance

In short

A new business unit should be a provisioning decision, not a project kickoff.

The situation
A group with several brands, client products and business units, each needing a public presence, an administrative surface and somewhere to put its data.
What made it hard
Built separately, every one of them restates routing, identity, content models and admin screens — and every restatement is another place where governance can drift and a tenant boundary can be got wrong.
The idea
Treat the group itself as the product: one platform that owns identity, tenant context, configuration and provider access, and lets each brand or client differ only in what is genuinely theirs.
What exists
A multi-tenant foundation is live — tenant-addressed storage, separated platform and tenant settings, and role-decided administration. Operational products are in progress; the group operating platform and tenant-safe reporting are not built yet.
Why it existsThe operating ideaThe capability modelThe tenant boundaryAdministrationHow it was builtDecisions and proofThe stackWhat exists todayWhat it proved

01

Why build a platform instead of another website?

Growing digital businesses tend to build each new website, product and client solution from zero. Routing, authentication, content models and admin screens get rebuilt every time, each slightly differently. The cost is not only the repeated engineering — it is that governance, data shape and provider access end up defined per project, so there is no single answer to who may change what, or where a given record lives.

  • Repeated delivery effort. Every new brand or client product restarts from scratch — routing, authentication, content models and admin screens rebuilt each time.

  • Inconsistent administration. Separate admin experiences per project mean different controls, different permissions, and no shared definition of who may change what.

  • Fragmented data and configuration. Content, settings and operational records spread across disconnected stores, with no shared shape to report on.

  • Ungoverned provider access. External providers and AI services get wired in per project, with credentials and entitlements handled inconsistently.

  • Projects instead of products. Bespoke engagements leave behind little reusable capability, so each delivery carries the full cost of the last.

02

An ecosystem, not an application

A shared platform provides the foundations — tenancy, identity, content, configuration, media and provider access — and each tenant composes its own public experience and operating model on top of them. A tenant brings its brand, content, catalogue, users and operational rules; the platform brings everything those have in common.

The platform separates the public experience, tenant administration and platform governance. A tenant can present its own brand, content, services and configuration without exposing or affecting another tenant's data.

03

What the operating model needed

Six capabilities, and how each one shows up in the platform rather than in a strategy document.

The capability model

6 connected capabilities

Strategy

Strategy here means deciding what is shared and what is allowed to differ. Products, tenants and delivery order are settled before implementation, so a new brand inherits the platform instead of commissioning one.

I set the operating model and the sequencing, and decided which capabilities belonged to the platform rather than to any one product.

  • Operating-model design
  • Capability sequencing
  • Tenant provisioning model

Read the delivery phases

Strategy

Strategy here means deciding what is shared and what is allowed to differ. Products, tenants and delivery order are settled before implementation, so a new brand inherits the platform instead of commissioning one.

I set the operating model and the sequencing, and decided which capabilities belonged to the platform rather than to any one product.

  • Operating-model design
  • Capability sequencing
  • Tenant provisioning model

Read the delivery phases

  • Strategy: Strategy here means deciding what is shared and what is allowed to differ. Products, tenants and delivery order are settled before implementation, so a new brand inherits the platform instead of commissioning one.
  • Governance: Decision rights are expressed in the platform rather than in convention. Platform settings and tenant settings are separate stores, and the role a user holds decides which of them they can reach at all.
  • Data: Every record carries its owning tenant, and every read is addressed through that tenant's namespace rather than filtered after the fact. Content, configuration and operational records share one validated shape across tenants.
  • Intelligence: Application events are being captured in a shape that can answer business questions later. The tenant-safe reporting layer itself is Phase 4 work and is not built yet.
  • Agents: AI and external providers are configured per business unit or tenant, and anything that writes retains a human approval step. Provider credentials are represented as secure references only — never exposed through the UI, logs or public APIs.
  • Outcomes: The point of the separation is faster launch of future brands and client platforms, less duplicated engineering and administration, and one place where governance and data quality are defined rather than six.

04

The multi-tenant model

Three layers, and one rule about how a request moves through them: the active tenant is resolved from the request host and the session, never from anything the browser supplies. Once resolved, it becomes the address every read and write is built from.

How a request resolves through the three platform layersA request arrives with a host and a session. The platform layer resolves it to a tenant; the tenant layer supplies the configuration addressed under that tenant's namespace; the public layer renders it. Layers, top to bottom: Platform level — Tenant provisioning and global settings, Shared templates and component standards, Platform administrator roles, Provider governance and entitlement rules. Tenant level — Brand, domain, theme and homepage content, Services, products, pricing and promotions, Tenant users and role assignments, Approved provider access. Public / customer level — Branded public experience, Booking and purchasing flows, Privacy-respecting analytics, Customer communications and consent.Incoming request — host + sessionPlatformsharedResolve tenant from host + sessionTenant provisioning and global settingsShared templates and component standardsPlatform administrator rolesProvider governance and entitlement rulesTenantisolatedAddress the read: sites/{tenant}/…Brand, domain, theme and homepage contentServices, products, pricing and promotionsTenant users and role assignmentsApproved provider accessPublicrenderedRender that tenant's pageBranded public experienceBooking and purchasing flowsPrivacy-respecting analyticsCustomer communications and consent

The platform layer provisions tenants and holds what they share. The tenant layer holds everything one tenant configures for itself. The public layer is what that configuration renders to.

A tenant identifier arriving in a query string, a form field or a header is ignored — it plays no part in deciding what gets read.

05

Two consoles, one set of decision rights

The administration layer is a product in its own right. A platform console provisions and governs tenants; each tenant gets a workspace scoped to its own presence and operations. Navigation is role-aware, so users see only the tenants and actions they are authorised to reach, and common changes are configuration rather than deployments.

  • Platform Admin Console. Create, configure, suspend and manage tenants, and govern provider access.

  • Tenant Admin Workspace. Manage one tenant's public presence, content, catalogue and operations.

  • Role-aware navigation. Users see only the tenants and actions their platform and tenant roles permit.

  • Configuration-first operations. Branding, content, services and publishing change without a code deployment.

Decision rights by administration area.
Admin areaPlatform administratorTenant administrator
Create tenantYesNo
Assign tenant templateYesNo
Configure global standardsYesNo
Manage tenant brandingOptional oversightYes
Manage tenant contentOptional oversightYes
Manage services / productsOptional oversightYes
Manage tenant usersYes / governedYes, within tenant
Configure providersYesRequest or use approved providers
View cross-tenant dataYes, authorised onlyNo
Publish tenant changesGovernedYes, within tenant

06

Delivery approach

  1. Phase 1

    Live

    Multi-tenant foundation

    • Tenant routing and isolation
    • Tenant settings, theme and content models
    • Public and admin surfaces
    • Role-aware authentication and authorisation
  2. Phase 2

    In progress

    Operational products

    • Configurable services, products, bookings and customer journeys
    • Media, communications and payment integrations
    • Tenant reporting and operational workflows
  3. Phase 3

    Planned

    Group operating platform

    • Business-unit provisioning
    • Central provider governance
    • SGOS control-plane integration
    • Auditing, approvals and human-in-the-loop workflows
  4. Phase 4

    Planned

    Scalable intelligence

    • Shared analytics conventions
    • Tenant-safe reporting
    • Data products and AI-enabled capabilities

07

The part worth checking

Isolation is the claim this platform rests on, so it is the one that should be shown rather than asserted. Below: where the boundary is enforced, what the design costs, and the mistake that turned it from an assumption into a tested boundary.

Where tenant scoping is enforced

src/lib/projects/paths.ts · src/lib/projects/blob.ts
// Every object a tenant owns is addressed through its slug. The prefix is
// part of the address, not a filter applied afterwards.
export function getProjectRootPath(siteSlug: string): string {
  return `${getSiteRootPath(siteSlug)}/projects`;
}

export function getProjectMetaPath(siteSlug: string, slug: string): string {
  return `${getProjectRootPath(siteSlug)}/${slug}/meta.json`;
}

/** A record must be stamped with the tenant whose namespace it was read from.
 *  Anything else is treated as missing rather than returned. */
function belongsToTenant(
  meta: { tenantId?: string; tenantSlug?: string },
  tenant: ProjectTenantRef
): boolean {
  if (tenant.id && meta.tenantId) {
    return meta.tenantId === tenant.id;
  }
  return !meta.tenantSlug || meta.tenantSlug === tenant.slug;
}

The second check is redundant if the first one is right — which is the point of it. It catches a path built from the wrong slug, and records written before tenant stamping existed. A read that fails it returns nothing, rather than another tenant's record.

Decision

Tenant-prefixed object storage, not a tenant column on every row

Chose
Each tenant's catalogue lives under its own storage prefix — sites/{tenantSlug}/projects/… — with a per-tenant index file. A read cannot accidentally span tenants, because the tenant is part of the address rather than part of a WHERE clause somebody can forget.
Instead of
One relational table with a tenantId column and a scoped query on every read. That is the conventional answer, and the one the ORM makes easiest.
What it costs
No joins, and no cheap cross-tenant query. Answering "show me everything across the group" means fanning out across prefixes and merging in memory. Each write also has to keep a per-tenant index consistent by hand, so the index is rebuilt from the underlying objects whenever it drifts.
What would change it
The first feature that genuinely needs cross-tenant reporting. That is Phase 4 work, and it moves the catalogue to Postgres with row-level scoping — which is why the record shape is already validated in one schema rather than at each call site.

Request caching was one shared object away from a cross-tenant read

The project snapshot is wrapped in React's cache() so a single request does not re-read storage repeatedly. cache() keys on argument identity, not value — and the tenant was being passed in as an object. Two calls holding equal-but-distinct tenant objects simply miss the cache, harmlessly, which is why it looked fine. Two calls that share one tenant object across a request boundary do not miss: the second tenant is served the first tenant's catalogue. Nothing in the type system distinguishes the two cases.

The tenant is now flattened to a string key before it reaches cache(), and expanded again inside. More usefully, isolation stopped being an assumption about how the code gets called and became a test file of its own: cross-tenant reads, edits, project counts, featured ordering, slug collisions between tenants, and related-project links pointing outside the tenant — fifteen tests written at the boundary rather than on the helpers.

08

The stack, and how far each part has got

Technology choices favour fast iteration without giving up multi-tenant boundaries or structured data ownership. Each item carries its current maturity.

  • Next.js App Router

    Live

    Server-rendered public and admin surfaces on one routing model.

  • React

    Live

    Component model shared across tenant experiences.

  • TypeScript

    Live

    Typed contracts between data, services and UI.

  • Tailwind CSS

    Live

    Design-system tokens and consistent, responsive layout.

  • Prisma ORM

    In progress

    Typed, tenant-scoped data access for relational models.

  • Postgres (Neon / Vercel Postgres)

    Live

    Tenant registry, identity, membership and operational records.

  • Role-aware authentication

    Live

    Sessions bound to a tenant, with platform and tenant roles.

  • Object storage

    Live

    Tenant media and content documents under per-tenant namespaces.

  • Resend

    Live

    Controlled transactional email delivery.

  • PayFast

    Planned

    Payment capture for tenants that sell directly.

  • Vercel

    Live

    Deployment with separated preview and production environments.

09

What exists today

The platform is mid-build. What is real is the foundation: the boundaries, the shared record shapes, and the administration split.

  • Tenant isolation, under test. Fifteen tests cover cross-tenant reads, writes, counts and relations; seventy across the catalogue module as a whole. Two tenants can hold the same project slug without collision.

  • One validated record shape. Projects, case-study blocks and SEO metadata are parsed through a single schema on write and on read, so a malformed stored record fails loudly instead of rendering as a half-empty page.

  • Administration split in the model, not the UI. Platform and tenant capabilities are resolved server-side from the actor's role and the resolved tenant. The navigation follows that result; it does not decide it.

10

What the build has actually proved

The platform is far enough along to have been tested by its own mistakes. What follows is what that has shown, what changed because of it, and what has not been built.

  • A tenant boundary holds only when it is part of the address. Tenant-prefixed storage removed a whole class of forgettable WHERE clause.

  • Decision rights are easier to reason about as two separate stores than as one store with conditional access.

  • Stating maturity honestly per component is what makes the rest of the case study worth reading.

Request caching was one shared object away from a cross-tenant read

At first
The project snapshot was wrapped in React's cache() with the tenant passed in as an object, to avoid re-reading storage repeatedly within one request.
What that showed
cache() keys on argument identity rather than value. Two equal-but-distinct tenant objects miss the cache harmlessly, which is exactly why the bug stayed invisible — but two calls sharing one tenant object across a request boundary would serve the second tenant the first tenant's catalogue.
What it is now
The tenant is now flattened to a string key before it reaches cache(), so identity and value coincide. The isolation is enforced by the key itself rather than by the caller remembering to construct a fresh object.

What has not been built

  • Operational products

    In progress

    Configurable services, products, bookings and customer journeys

  • Group operating platform

    Planned

    Business-unit provisioning

  • Scalable intelligence

    Planned

    Shared analytics conventions

Next steps

This is how I approach modern digital delivery: start with the operating model, build the boundaries first, and make the claims checkable.

Discuss a platform buildView related work
All projects
Engagement27 views

React to this

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment

0/2000