Yesterday I started a live coding thread on twitter. I implemented multi tenancy into a feathers app. Live coding thread basically means tweeting every step of the proccess and explaining it in a twitter thread. I like this concept a lot and will be doing more of them soon. If you like this concept too, make sure to connect on twitter.
What does multi tenancy mean?
In software development multi tenancy often means that a single application is hosted on one server but serves different tenants. Tenant can mean different things eg a tenant can be a company, an app or a team. Each tenant will use the application as if it's using it alone. It is not connected to other tenants in any way.
What I built
I'm currently building the backend as a service for kiss.js (see my other articles) which is called kisscloud. A single kisscloud instance can host multiple applications. A classic multi tenancy use case.
Kisscloud uses feathers.js under the hood. So what I'm actually doing is adding multi tenancy to a feathers.js app.
What needs to be done?
Kisscloud will update everything in realtime. So it uses websockets. In order to add multi tenancy to a websocket based app the follwing steps need to be done:
- I will hook into the socket handshake and add the kissjs appId to the socket connection.
- I will overwrite the default feathersjs authentication to use the appId when requesting session tokens and creating new users.
- And finally I will also add the appId restrictions to every data resource
Adding the appId to the socket connection
Adding the appId to the socket connection is actually pretty easy.
On the client I just had to pass it when the socketio connection got configured.
And on the server I could register a very simple middleware that gets executed for every incoming websocket.
const socket = io(options.endpoint, {
query: {
appId: options.appId
}
});
this.feathers = feathers();
// Setup the transport (Rest, Socket, etc.) here
this.feathers.configure(socketio(socket));
This is a basic feathers setup. The only thing that happened here is adding the appId to the socket handshake by adding it to the query object.
On the server it's even simpler. This little middleware gets executed for every incoming socket connection. It reads the appId from the handshake and saves it for later use.
app.configure(socketio(function (io) {
io.use(function (socket: any, next) {
socket.feathers.appId = socket.handshake.query.appId;
console.log("received appId: " + socket.handshake.query.appId);
next();
});
}
));
The cool part is the feathers attribute on the socket object.
It's handled by feathersjs and is made available to almost everything you can imagine. This will come in handy when we try to get access to the appId later.
Access control for data
Access control is very easy with feathersjs. I created 2 hooks, the first adds the appId(already saved to connection after socket init) to every saved resource.
import { Hook, HookContext } from '@feathersjs/feathers';
export default (options = {}): Hook => {
return async (context: HookContext) => {
context.data = {
...context.data,
app_id: context.params.appId
}
return context;
};
}
And the second one forces to query for the given appId whenever a query is made for any resource.
import { Hook, HookContext } from '@feathersjs/feathers';
export default (options = {}): Hook => {
return async (context: HookContext) => {
context.params.query = {
app_id: context.params.appId
}
return context;
};
}
That's basically it to ensure that only resources belonging to an app can get loaded and saved.
Now the tricky part:
Authentication
When signing in, I need to ensure that I query for the username based on the appId. This is pretty easy with feathers. i can extend the local authentication strategy used by feathers and also query for the appId. This way I always load the correct user based on username and on appId:
import { LocalStrategy } from '@feathersjs/authentication-local';
import { Params, Query } from '@feathersjs/feathers';
export class MultiTenantLocalStrategy extends LocalStrategy {
async getEntityQuery(query: Query, params: Params) {
// Query for appId, too
return {
...query,
app_id: params.appId,
$limit: 1
}
}
}
The heaviest part of all this was creating a new user. The problem is that feathersjs handles the uniqueness of usernames/emails on the database layer. I want to stay database independent with kissjs. So I had to bypass this...
First I removed the uniqueness index from the db. At this point there could be unlimited registered users with the same username. What I want is unlimited users with the same username, but each with a different appId.
I created another hook for this that gets executed every time a user gets created or updated.
import { Hook, HookContext } from '@feathersjs/feathers';
export default (options = {}): Hook => {
return async (context: HookContext) => {
var appId = context.data.app_id;
var username = context.data.username;
try {
var duplicate = await context.app.service("users").find({
query: {
app_id: appId,
username: username
}
})
if (duplicate.data.length !== 0) {
throw new Error("username already taken")
}
} catch (e) {
console.log("error: " + e)
throw e;
}
return context;
};
}
This hook loads a user based on the given username and adds the appId to the query. If a username already exists, the signup flow gets interrupted here. This is not optimal of course, but I think I can live with this solution for now. I can always easily switch back to the database layer when I use a database that can handle unique constraints based on several attributes.
And that's it. With this small changes to the codebase I have complete multi tenancy support.
If you have any additional questions or want to build something similar and need help, just let me know.
I share as much stuff as possible about the progress on kissjs on twitter. If you're interested in how to build a javascript framework and corresponding backend as a service, you might want to follow me there.
Top comments (4)
FeathersJS ! That's an underappreciated framework.
Can you explain more the reason why?
Because it's a great framework with interesting concepts and a very dedicated community around it. But I don't see it promoted very often, other frameworks seem to be more in the spotlight. I dabbled a bit with it but unfortunately the project in which I was intending to use it never took off.
Oh thank you, sir