Building SaaS apps with Slash (and open source Dgraph)

Has anyone created a SaaS app with the current versions of Slash or Dgraph and have any insights on how you approached setting up authorization and accounts/tenants/workspaces and their users/members?

I’ve searched for info about multi-tenancy and creating SaaS apps with Dgraph and it looks like a) there may be future multi-tenancy support at the db namespace level possibly in the works (and possibly Enterprise edition only?) and b) that there may be little or no info and docs about using current Slash and Dgraph tools for roll-your-own SaaS account/workspace management? Is that true?

I’d assume when going the roll-your-own route, the schema (before @auth filters) might look something like this?:

type Tenant  {
	id: ID!
	name: String! @search(by:[fulltext])
	members: [User!]
	nodeX: NodeX @hasInverse(field:tenant)
	nodeX: NodeY @hasInverse(field:tenant)
	nodeZ: NodeZ @hasInverse(field:tenant)
}

type User  {
	id: ID!
	username: String! @search(by:[hash]) @id
	name: String @search(by:[exact])
	tenants: [Tenant!] @hasInverse(field:members)
}

type NodeX  {
	id: ID!
	name: String @search(by:[fulltext])
	tenant: Tenant! @hasInverse(field:nodeX)
}

type NodeY  {
	id: ID!
	name: String @search(by:[fulltext])
	tenant: Tenant! @hasInverse(field:nodeY)
}

etc...

And regarding the current state of Dgraph, in your opinion, is it a good choice or is it a “just not ready yet” choice for building SaaS apps?

:raising_hand_man:

Check out this blog posts

I’m afraid there’s a bit of miscommunication here. Multitenancy would allow many different schemas on the same database instance. It’s coming in the next version of Dgraph Enterprise and Dgraph Cloud.

With Multitenancy, you wouldn’t need

type Tenant {...}

Each tenant would have their own schema (User, NodeX, NodeY etc). And their own auth stuff.

There’s a notion of ACL - each tenant can only logon to their own namespace, with the Guardians of the Galaxy (ugh, don’t @ me, I don’t like the name either) being a “super admin” of sorts that is used to manage the DB.

Right… there is a big misconception around tenants and graphs all the way around. The one thing missing from the upcoming multi-tenant is querying across namespaces. But if all you need is a single schema with a tight auth control so a user only gets what is theirs when they login then that is within the power of Dgraph without multi-tenant. Then you can do some pretty amazing stuff like working up a recommendation engine to other data a user might be interested in through connections that the user does not have access to. Querying across a virtual namespace but locking a user down to within their own virtual namespaces. In my blog I show how it works for a 3 tier namespace (private, groups, and public)

Querying across namespace sounds like a security disaster waiting to happen.

I am showing off our SaaS soon :slight_smile: at the Graph Day

@chewxy If not implemented correctly, it could be. But so is a graph with auth rules if not implemented correctly. SQL has namespaces in the form of separate databases on a single server, but still allows a user who has access to multiple databases to join across them. This was our database setup before Dgraph. Every user had their own database and a database for managing users and a public database. It worked well for what we needed at the time until we started growing. Managing 500+ databases when you need to add a new feature was not fun. Nor was trying to run usage statistics from all of them together. They all shared mostly the same schema but were separated for security purposes. A user could never get data from another users database because we locked them down to just the databases they were allowed to access.

Do you use namespacing or virtual namespacing (i.e. the one a programmer enforces on top of a single tenant DB)

I say virtual namespace meaning the graph of data a user is able to query based upon the auth rules in the graph.

I think the upcoming change to locking down the API based upon the client API key will do a lot for virtual namespaces as well. Right now a user can see a bunch of the API that they don’t have access to use, so not showing those parts of the API based upon a clients API key would benefit as well. It would be an almost two factor authentication. But not really 2fa because both are same type (what you know)

1 Like

Thanks everyone for the info.

@amaster507 thanks, I’ll dive into your resource. Do you happen to know of any similar resources specifically created for SaaS products which have a slightly different structure of tenants/accounts/workspaces → groups of users/members of that tenant/account/workspace… (and may have additional ACL access levels within those walled-off tenant/account/workspace groups)?

Yeah @chewxy good point: I should have been more clear about the type of SaaS I was referring to building:. In my case: the common multi-tenant, single-schema SaaS:

SaaS

I’m referring to this common form of SaaS:

Multi-tenant
Each customer/tenant (most commonly a business customer) has it’s own walled-off data, member users etc.

Single schema
Core service has a single data schema shared by all of these tenants/customers. Updating a feature and data schema applies to all tenants/customers.

Examples:
Slack and team chats, Intercom and site chats, MailChimp and email service providers, etc.

IaaS, PaaS, BaaS

I’m not referring to these subsets of SaaS. Infrastructure as a service, platform as a service and backend as a service. Whereas the broader SaaS services target all types of teams within the business (sales, marketing, HR, customer success, product etc), these Iaas, PaaS, BaaS subsets usually target developers and engineers. Here you might be more likely to find multi-tenant where each tenant has their own data schemas, their own separate DBs, or possibly even single-tenant architecture, yeah? Examples: offerings from AWS, Dgraph Slash, Hasura etc.

So limiting this to the first type of Saas above, the multi-tenant, SINGLE-schema SaaS app:

  1. Using Slash/DGraph’s CURRENT functionality, something like the schema above might be the way to go?

  2. Even with Dgraph’s future namespaced, db-level multi-tenancy, if someone is building a multi-tenant, SINGLE-schema SaaS app, it would still be wise to use the roll-your-own approach above or would using the namespaced, db-level multi-tenancy be wise?

  3. Has anyone created a multi-tenant, single data schema SaaS app in Slash/Dgraph and have any 30,000 foot view architecture or lessons learned to share?

  4. Does anyone know of any documentation, blog posts, example repos or other resources that dive into using Slash/Dgraph to build a multi-tenant, single schema SaaS app?

Thanks again everyone for your help!

Yep, this is discussed in my blog I posted above, I do not use the tenant/workspace terminology, but the examples should go well towards answering your questions.

Here is something I whipped up real quick that uses that terminology

type User @auth(
  query: { or: [
    { rule: "query ($username: String!) { queryUser(filter: { username: { eq: $username } }) { username } }" }
    { rule: "{$role: { eq: \"admin\" }}" }
  ]}
  add: { or: [
    {rule: "{$role: { eq: \"admin\" }}" }
    {rule: "{$role: { eq: \"register\" }}" }
  ]}
  update: { rule: "{$role: { eq: \"admin\" }}" }
  delete: { rule: "{$role: { eq: \"admin\" }}" }
) {
  id: ID
  username: String! @id
  name: String @search(by:[exact])
  hasTenantAccess: [TenantACL] @hasInverse(field: "grantedTo")
}

type Tenant @auth(
  query: { rule: "query ($username: String!) { queryTenant { grantedAccess { grantedTo(filter: { username: { eq: $username } }) { username } } } }" }
  add: { rule: "query ($username: String!) { queryTenant { grantedAccess(filter: { withLevel: { eq: owner } }) { grantedTo(filter: { username: { eq: $username } }) { username } } } }" }
  update: { rule: "query ($username: String!) { queryTenant { grantedAccess(filter: { withLevel: { eq: owner } }) { grantedTo(filter: { username: { eq: $username } }) { username } } } }" }
  delete: { rule: "{$role: { eq: \"admin\" }}" }
) {
  id: ID!
  name: String! @search(by:[fulltext])
  grantedAccess: [TenantACL] @hasInverse(field: "permissionOn")
}

enum AccessLevel {
  owner
  moderator
  viewer
}

type TenantACL @auth(
  query: { or: [
    { rule: "query ($username: String!) { queryTenantACL { grantedTo(filter: { username: { eq: $username } }) { username } } }" }
    { rule: "query ($username: String!) { queryTenantACL { permissionOn { grantedAccess(filter: { withLevel: { in: [owner,moderator] } }) { grantedTo(filter: { username: { eq: $username } }) { username } } } } }" }
  ] }
  add: { rule: "query ($username: String!) { queryTenantACL { grantedTo(filter: { not: { username: { eq: $username } } }) { username } permissionOn { grantedAccess(filter: { withLevel: { in: [owner,moderator] } }) { grantedTo(filter: { username: { eq: $username } }) { username } } } } }" }
  update: { rule: "query ($username: String!) { queryTenantACL { grantedTo(filter: { not: { username: { eq: $username } } }) { username } permissionOn { grantedAccess(filter: { withLevel: { in: [owner,moderator] } }) { grantedTo(filter: { username: { eq: $username } }) { username } } } } }" }
  delete: { rule: "query ($username: String!) { queryTenantACL { grantedTo(filter: { not: { username: { eq: $username } } }) { username } permissionOn { grantedAccess(filter: { withLevel: { in: [owner,moderator] } }) { grantedTo(filter: { username: { eq: $username } }) { username } } } } }" }
) {
  permissionOn: Tenant!
  grantedTo: User!
  withLevel: AccessLevel
}

type NodeX @auth(
  query: { rule: "query ($username: String!) { queryNodeX { tenant { grantedAccess { grantedTo(filter: { username: { eq: $username } }) { username } } } } }" }
  add: { rule: "query ($username: String!) { queryNodeX { tenant { grantedAccess(filter: { withLevel: { eq: owner } }) { grantedTo(filter: { username: { eq: $username } }) { username } } } } }" }
  update: { rule: "query ($username: String!) { queryNodeX { tenant { grantedAccess(filter: { withLevel: { in: [owner,moderator] } }) { grantedTo(filter: { username: { eq: $username } }) { username } } } } }" }
  delete: { rule: "query ($username: String!) { queryNodeX { tenant { grantedAccess(filter: { withLevel: { in: [owner,moderator] } }) { grantedTo(filter: { username: { eq: $username } }) { username } } } } }" }
) {
  id: ID
  name: String @search(by:[fulltext])
  tenant: Tenant!
}

type NodeY @auth(
  query: { rule: "query ($username: String!) { queryNodeY { tenant { grantedAccess { grantedTo(filter: { username: { eq: $username } }) { username } } } } }" }
  add: { rule: "query ($username: String!) { queryNodeY { tenant { grantedAccess(filter: { withLevel: { eq: owner } }) { grantedTo(filter: { username: { eq: $username } }) { username } } } } }" }
  update: { rule: "query ($username: String!) { queryNodeY { tenant { grantedAccess(filter: { withLevel: { in: [owner,moderator] } }) { grantedTo(filter: { username: { eq: $username } }) { username } } } } }" }
  delete: { rule: "query ($username: String!) { queryNodeY { tenant { grantedAccess(filter: { withLevel: { in: [owner,moderator] } }) { grantedTo(filter: { username: { eq: $username } }) { username } } } } }" }
) {
  id: ID
  name: String @search(by:[fulltext])
  tenant: Tenant!
}

# Dgraph.Authorization ...

Rules Applied

  • Any NodeX belongs to only one tenant
  • Only a user granted (with access level) on the correlating tenant can query NodeX
  • Only a user granted access levels moderator or owner on the correlating tenant can update NodeX
  • Only a user granted access levels owner on the correclating tenant can delete NodeX
  • Only a user granted access level owner can add a NodeX for their tenant.
  • Copy above rules to NodeY, etc.
  • A user can be in multiple Tenants
  • Users can have different access levels inside a Tenant
  • A Tenant does not have to have any authorized users
  • A User does not have to belong to any tenants
  • A user cannot add a Tenant they do not own
  • A user can query their own access
  • A user can query access for tenants they own/moderate
  • A user cannot add other users to access a tenant that they do not own/moderate
  • A user cannot add/update/delete their own access
  • Only a global admin can delete tenants, or update, delete users
  • Only a global admin or authorized register client can add users

This assumes the authorized user will provide a signed JWT with their username in the custom claims.
Global admins will provide a signed JWT with a role set to “admin”.
Registration scripts will provide a signed JWT with a ‘role’ set to “register”

This setup requires a Tenant to be created first and then using the id of a tenant create NodeX, NodeY, etc…

3 Likes

Wow. @amaster507 that’s great. Thank you! Will dive in.

A question @amaster507: why no “id: ID!” for “type TenantACL”?

I see that Dgraph will create an id for that ACL node anyway, but because it’s not defined in the schema, then you have no way to access that ACL node to update it or delete it via GraphQL “filter: ID = X” mutations.

I can add “id: ID!” to “type TenantACL”, but wanted to make sure I’m not missing something! Thanks.

Sorry I overlooked it. Add it if you want to manage that type by id.