Organizations
Before we go into how Novel handles multi-tenancy, We'd love for you to check out this wonderful article by Andrew Culver, creator of Bullettrain, a rails starter kit
Novel comes with multi-tenancy enabled by default. This allows your SaaS customers to scope their actions based on an organization they belong with.
What this means is that any feature related to your SaaS should be developed with multi-tenancy in mind.
Data Model
There are 3 data models relevant to multi-tenancy
Organization - This is the organization that can have multiple users.
Accounts - A user account
Organization Membership - The membership detail of an account to an organization.
Novel implements a similar data model to the article written by Andrew Culver. A big difference is that subscriptions are by default, tied to an organization instead of the membership.
Signup
Novel will create an organization and an acount during sign up. This will also create a stripe customer and create a subscription for it against the organization created.
When the user logs in the next time, the organization will be attached to the session.
Reference: https://github.com/madewithnovel/novel/blob/main/app/features/lifecycle/signup.ts
Sessions
Sessions in Novel include both the currently logged in user and the organization it is currently using. They are accessible using the request.org
and request.account
property of the current request.
Switch Organizations
You are able to switch organizations in the same session by using the Switch Organization endpoint in the API reference below.
Subscriptions
When the user is created, an organization is created and a stripe customer is created along with it.
This is important so that there is representation of the organization in stripe.
Any changes to the subscription is attached to the organization only. This can only be done by users with the correct permission.
An example of this can be found here: https://github.com/madewithnovel/novel/blob/main/app/api/internal/v1/subscriptions-subscribe/index.ts
import * as upgrade from 'app/features/lifecycle/upgrade';
export default async function Route (instance) {
instance.authenticated();
instance.post('/api/v1/subscription/upgrade', handler);
async function handler (request, reply) {
await request.throttle();
const orgId = request.org.id;
/**
* See the implementation of the strategy in /app/features/lifecycle/upgrade.ts
*
* These may have additional requirements during creation like upfront payment
* and storing data. You may pass these to your upgrade lifecycle.
*/
const { plan, intent, method, interval } = request.body;
await upgrade.start(request.session.id, { customer: orgId, plan, intent, method, interval });
reply.status(204);
}
}
Actions
It is important to always check the association of the current user against its membership of the target organization it is trying to perform an action on.
This is available with the Organizations Model.
Authorizationimport * as Organization from 'novel/organization';
export default async function Route (instance) {
instance.authenticated();
instance.cached();
instance.get('/api/v1/organizations', handler);
async function handler (request) {
await request.throttle();
await request.can('read', request.org.id);
const organizations = await Organization.isMemberOf(request.account.id);
return { organizations };
}
}
Activities
An organization may have activities recorded against actions made to it.
This is stored in the organization_events
model. It is also available under the novel/activity
module.
import * as activity from 'novel/activity';
await activity.org('SOMETHING_NEW', 'Something happened');
This will get the organization ID through the request context.
Single-Tenancy
Single tenancy is not fully supported as of release 2025.1.0.
It is possible to do this however by removing the organization related endpoints from the /app/api/v1
directory.
This will force the user to sign up and interact with only organization.
If you feel that this is a feature that you would like to see, send us an email at [email protected].
Changelog
2024-12-20 - Initial Documentation
Last updated
Was this helpful?