Working with multi-tenancy projects

This guide will show you how to work with tenants; how to create them, source tenant specific events, work with Projection Definitions shared among tenants and read tenant specific Projection data.

What is a tenant?

In order to build business-to-business applications where the application serves many customers it is common to build a multi-tenant system. This means that multiple customers share the use of the same system instance, but their data is separated at the storage level. Serialized provides a convenient way for you to build multi-tenant systems by supporting this feature on project level.

Reasons for using multi-tenancy support

  • Keep data from different tenants completely separated
  • A tenant can be added or removed without affecting existing tenants
  • No need to change application code/deployment when growing your customer base

Setting up a project

To create a multi-tenant enabled project you need to have a paid B2B plan.

Go ahead and create a project and check the 'multi-tenant' checkbox.

Add your first tenants

When you create a new tenant you give it a unique ID and a reference. The reference is typically your own internal ID for that tenant (customer) or a human readable string, eg. customer name.

curl -i https://api.serialized.io/tenants \
  --header "Content-Type: application/json" \
  --header "Serialized-Access-Key: <YOUR_ACCESS_KEY>" \
  --header "Serialized-Secret-Access-Key: <YOUR_SECRET_ACCESS_KEY>" \
  --data '
  {
    "tenantId": "d876e2aa-05f6-4dbd-bcb6-14f3e9e160fd",
    "reference": "Alice Inc."
  }
'

Verify by listing your tenants, like this:

curl -i https://api.serialized.io/tenants \
  --header "Serialized-Access-Key: <YOUR_ACCESS_KEY>" \
  --header "Serialized-Secret-Access-Key: <YOUR_SECRET_ACCESS_KEY>"

The response should be:

{
  "tenants" : [ {
    "tenantId" : "d876e2aa-05f6-4dbd-bcb6-14f3e9e160fd",
    "tenantNumber" : "1",
    "addedAt" : 1599558817480,
    "reference" : "Alice Inc.",
    "deleted" : false
  } ]
}

Now, go ahead and add a second tenant:

curl -i https://api.serialized.io/tenants \
  --header "Content-Type: application/json" \
  --header "Serialized-Access-Key: <YOUR_ACCESS_KEY>" \
  --header "Serialized-Secret-Access-Key: <YOUR_SECRET_ACCESS_KEY>" \
  --data '
  {
    "tenantId": "e98c9115-4318-4e14-9949-1f05e29297cc",
    "reference": "Bob Inc."
  }
'

The list tenant call should now give you a response looking like this:

{
  "tenants" : [ {
    "tenantId" : "d876e2aa-05f6-4dbd-bcb6-14f3e9e160fd",
    "tenantNumber" : "1",
    "addedAt" : 1599558817480,
    "reference" : "Alice Inc.",
    "deleted" : false
  }, {
    "tenantId" : "e98c9115-4318-4e14-9949-1f05e29297cc",
    "tenantNumber" : "2",
    "addedAt" : 1599559106360,
    "reference" : "Bob Inc.",
    "deleted" : false
  } ]
}

Display tenants in the Console

When you log into the Web Console you will now see two tenants listed in a drop-down menu.

Store first event

Now, let's store an event for the "Alice Inc." tenant. In the example we'll create a Payment aggregate witch has two events defined: PaymentPerformed and PaymentVerified.

Note: We add the HTTP header Serialized-Tenant-Id to the request to indicate this event belongs to "Alice Inc.".

curl -i https://api.serialized.io/aggregates/payment/723ecfce-14e9-4889-98d5-a3d0ad54912f/events \
  --header "Content-Type: application/json" \
  --header "Serialized-Access-Key: <YOUR_ACCESS_KEY>" \
  --header "Serialized-Secret-Access-Key: <YOUR_SECRET_ACCESS_KEY>" \
  --header "Serialized-Tenant-Id: d876e2aa-05f6-4dbd-bcb6-14f3e9e160fd" \
  --data '
  {  
     "events": [  
        {  
           "eventType": "PaymentPerformed",
           "data": {  
              "amount": 12345
           }
        }
     ]
  }
'

Verify the event in the Console

Refresh your browser page to make sure the payment aggregate has been created with a single event.

Store next event batch

Ok! Let's store two events, at the same time, for the "Bob Inc." tenant.

curl -i https://api.serialized.io/aggregates/payment/ff274bb0-435b-4952-924e-739555a3274b/events \
  --header "Content-Type: application/json" \
  --header "Serialized-Access-Key: <YOUR_ACCESS_KEY>" \
  --header "Serialized-Secret-Access-Key: <YOUR_SECRET_ACCESS_KEY>" \
  --header "Serialized-Tenant-Id: e98c9115-4318-4e14-9949-1f05e29297cc" \
  --data '
  {  
     "events": [  
        {  
           "eventType": "PaymentPerformed",
           "data": {  
              "amount": 23456
           }
        },
        {  
           "eventType": "PaymentVerified",
           "data": {  
              "verifiedBy": "Chuck"
           }
        }
     ]
  }
'

Verify the events in the Console

We can now see that there are two events stored for "Bob Inc.", but that they were stored at the same time in the same batch.

Projecting events

Projection Definitions, i.e. the configuration that decides how events should be transformed into useful read-models, are shared among tenants. This means that any change you do to a Projection Definition will update all tenants projections.

Let's create a simple Projection Definition making it easy for us to query payments by status:

curl -i https://api.serialized.io/projections/definitions \
  --header "Content-Type: application/json" \
  --header "Serialized-Access-Key: <YOUR_ACCESS_KEY>" \
  --header "Serialized-Secret-Access-Key: <YOUR_SECRET_ACCESS_KEY>" \
  --data '
  {
  "projectionName": "payments",
  "feedName": "payment",
  "handlers": [
    {
      "eventType": "PaymentPerformed",
      "functions": [
        {
          "function": "merge"
        },
        {
          "function": "set", 
          "targetSelector": "$.projection.status",
          "rawData": "NEW"
        }
      ]
    },
    {
      "eventType": "PaymentVerified",
      "functions": [
        {
          "function": "merge"
        },
        {
          "function": "set",
          "targetSelector": "$.projection.status",
          "rawData": "COMPLETED"
        }
      ]
    }
  ]
}
'

Listing projections

Start with listing the projections for "Alice Inc.".

curl -i https://api.serialized.io/projections/single/payments \
  --header "Serialized-Access-Key: <YOUR_ACCESS_KEY>" \
  --header "Serialized-Secret-Access-Key: <YOUR_SECRET_ACCESS_KEY>" \
  --header "Serialized-Tenant-Id: d876e2aa-05f6-4dbd-bcb6-14f3e9e160fd"

The result should be:

{
  "projections" : [ {
    "projectionId" : "723ecfce-14e9-4889-98d5-a3d0ad54912f",
    "createdAt" : 1599595291024,
    "updatedAt" : 1599595291025,
    "data" : {
      "amount" : 12345,
      "status" : "NEW"
    }
  } ],
  "totalCount" : 1,
  "hasMore" : false
}

Now, let's try to list the projections for "Bob Inc.".

curl -i https://api.serialized.io/projections/single/payments \
  --header "Serialized-Access-Key: <YOUR_ACCESS_KEY>" \
  --header "Serialized-Secret-Access-Key: <YOUR_SECRET_ACCESS_KEY>" \
  --header "Serialized-Tenant-Id: e98c9115-4318-4e14-9949-1f05e29297cc"

...and the result should be:

{
  "projections" : [ {
    "projectionId" : "ff274bb0-435b-4952-924e-739555a3274b",
    "createdAt" : 1599595291063,
    "updatedAt" : 1599595291064,
    "data" : {
      "amount" : 23456,
      "status" : "COMPLETED",
      "verifiedBy" : "Chuck"
    }
  } ],
  "totalCount" : 1,
  "hasMore" : false
}

Notice how the status is different! We just showed that a single Projection Definition is shared and applies across tenants!

Check the full API documentation for details.