Tenant-specific URLs with AWS

Using a DynamoDB lookup table and Lambda@Edge to serve tenant-specific URLs from the edge with CloudFront

Ronny Roeller
NEXT Engineering
3 min readMar 16, 2023

--

Photo: Ronny Roeller

Why tenant specific URLs?

We offer our customers tenant-specific URLs like customer1.nextapp.co to customize the experience before the user signs in. This allows a.o.:

  • Giving user confidence that they sign into the right place by showing the customer’s name and logo on the login page
  • Directly offer customer specific sign-in options, e.g. users in a SSO-only tenant can be forwarded to the customer’s IdP
  • Disable any usage tracking to ensure compliance with customer’s policies
  • Allow the customer’s IT team to limit access to their tenant-specific subdomain

Challenge: Where is the tenant?

Bringing the tenant in the URL enables us to derive the tenant from the URL before a user signs in. Yet, from where do we actually get the tenant-specific information like the customer logo?

And we have an additional complication: Our tenants are distributed over various AWS accounts to ensure strong data separation (see this article to learn more). This means that, for example, customers’ names and logos aren’t stored all in one DynamoDB table but across various tables across many different AWS accounts.

First attempt: Dynamic Lookup

Initially, we served the tenant-independent shell of the app as a static HTML/JavaScript files directly from S3/CloudFront. Once loaded in the browser, the app would query tenant-specific information by sending multiple API requests, providing the tenant as a parameter.

Our API went then to the various AWS accounts, tried to find the tenant, and then looked up the required information. Although we used Lambda@Edge function for this logic, we couldn’t fully leverage CloudFront caching because the Lambda@Edge function didn’t know if any data changed. For example, we had to assume that the customer might have uploaded a new logo each time— because the function just wasn’t aware about it.

This approach initially did its job but it became slower and slower as we acquired more tenants and required more data points. Going through the loops of requesting data from the API often ended up taking ~5sec.

Far from a great first experience for our users.

Let’s try again: Lookup Table

The Dynamic Lookup approach suffered from two issues:

  • Failed to leverage the lifetime of data points: For example, customers very seldom upload a new tenant logo. Yet, Dynamic Lookup optimizes for this happening every second.
  • Too strict data separation: Dynamic Lookup assumed that none of the data points could be stored centrally. Yet, everything shown on the login page is public by nature.

So, we decided for a smarter caching that took the above points into account.

We created a central lookup table in DynamoDB that contains the public data points for each tenant. We fully denormalized the data to optimize for fast querying. Most importantly, the table is not updated on querying (like Dynamic Lookup) but whenever a system changes one of the field. For example, only if the customer changes its logo, will the table be updated.

Having the tenant data available for fast querying, also enabled us to eliminate the additional API requests for lookup. Now, we inject the tenant data straight into the — otherwise — static HTML. This allows CloudFront to cache the tenant data directly at the edge.

We plan on optimizing this further by using DynamoDB global tables, so that the table lookups are equally fast all around the globe.

Was it worth it?

We saw pretty spectacular results with our Lookup Table approach:

  • The earlier 5 secs for lookups went down to <100ms for cached pages (and <1sec for initial requests).
  • As a nice bonus, the code turned out to be way easier, allowing us to remove 3,000+ lines of complex code from our stack.

Happy coding!

--

--

Ronny Roeller
NEXT Engineering

CTO at nextapp.co # Product discovery platform for high performing teams that bring their customers into every decision