Easiest subscription management for nodejs.
##Overview Subscriber is the subscriptions manager for nodejs and expressjs apps!
var subscriber = require('subscriber'), express = require('express'), app = express(), db = require('./myDbSetup');
app.use(subscriber.init({db:db,timeout:60}));
app.listen(3000);
Done! You now have a subscription management service filtering every request and deciding whether or not it fits within the requestor's plan or subscription.
##Installation
npm install subscriber
Doesn't get easier than that!
While installation is easy, you probably want to spend a bit of time before using subscriber to think about how you want to offer plans and what you want to restrict. After all, subscriber is responsible for enforcing your plan restrictions, not deciding what they should be. For that, please see the future.
You will usually have one or more plan "types", each of which has a name and restrictions. Let's create an example of a consulting client management service, with three tiers:
Each login will be affiliated with one plan, describing what it can do. So user "joe" logging in will retrieve that he is on plan "regular" and thus be allowed to maintain up to 10 clients.
You need to decide in advance (but can change at any time):
Often, Web services offer a time-limited free trial of paid plans (since offering free trials of free plans really does not make much sense!). For example, if we are offering three paid plans - bronze, silver, gold - we may want to allow a new user to try any plan for 14 days, as long as s/he has never tried a plan before.
subscriber allows you to support time-limited free trials, as well as arbitrarily extend the length of a particular trial. You do need to decide in advance (but can change at any time):
If you want, you can have subscriber enforce expiry dates for plans, by indicating on a user when the plan expires. Just say, "this plan expires on date X", and subscriber will enforce the user not being on the plan after that date.
This section lists all of the potential states that a user can be in, assuming the user has an account.
##Usage
subscriber provides control over requests based on different subscriptions. For example, if your Web service, say a consulting client management service, has three tiers: free, regular, pro. Free users get to have up to 3 clients, regular users up to 10 and pro up to 30. You want to ensure that each tier of users gets restricted to the amount they are allowed.
subscriber can also handle free trials. You may want to give users a free trial for 14 days (or maybe 25 days) of one or more of your paid plans, but only once.
subscriber controls each and every access to make sure it conforms with the subscriber's subscription rights.
subscription is related to security, but it isn't about security. Security Authorization is concerned with whether or not user "john" has the security right to read user "sally"'s clients. Subscription is about whether or not user "john" can create another client, even one he normally would be permitted, because he hasn't yet paid for a plan that lets him.
It is really really really good to keep subscription and security authorization separate. If you make a mistake with subscriptions, well, you accidentally gave away a little something too much for free, or didn't let a customer do something for which he paid; he will call support to complain, I guarantee it!
If you make a mistake with security, user "john" can see the private information of user "sally" or even change her information. Chances are "john" and "sally" are leaving you and never coming back! And if it makes the press, well, so is everyone else.
var express = require('express'), app = express(), subscriber = require('subscriber');
// set up my database
db = dbSetUp();
app.use(mySecurityPackage);
app.use(subscriber.init({db:db,timeout:60}));
app.use(app.router);
app.listen(3000);
Personally, I would always recommend your security authorization package, e.g. cansecurity, come before subscriber. You are better off preventing someone from doing something they would never be allowed to do before bothering to check their plan.
var subscriber = require('subscriber');
That shouldn't surprise anyone!
So what do I do with subscriber
once I have require
d it?
app.use(subscriber.init(config)); // returns subscriber, so you can chain
And what exactly goes in that config, anyways?
db
: a database object. Required. See below.timeout
: how long (in minutes) to keep the list of plans around before refreshing. Optional. Defaults to 60. Set to 0 for no caching in subscriber.base
: string. Base path after which a particular resource comes. Optional. Defaults to '/'. See below.paths
: object. Paths for each resource for a plan. See below.When you first initialize subscriber, it issues a request to get all of the plans, and caches them. Thus, it knows exactly how many clients are allowed under the "bronze" plan and how many groups under the "premium" plan. It keeps this cache for as long as the timeout
in the config. The next request that comes after the timeout is expired triggers a refresh. If the timeout is 0, it never caches (at least subscriber
doesn't).
With each request of any type (GET
, PUT
, PATCH
, POST
, DELETE
), subscriber will get the user information, and compare it to the plan in the cache (refreshing if past timeout).
If the request is within the plan, subscriber
will just call next()
. If it is not, it will return a 403
error. The body of the error will be the plan and the item that was limited:
{reason:"subscription",plan:"bronze",limit:"client",maximum:5}
In the above example, the user attempted to create too many items of type client, with the plan "bronze", where the limit is 5.
The db
instance passed to subscriber.init()
is the key to subscriber getting information about:
The combination of all of the above allows subscriber to determine whether or not the user is permitted to perform the action s/he wants.
subscriber expects the db
to have two asynchronous methods: plans()
and user()
db.plans(callback)
should retrieve all of the known plans with their limits, as well as trial information, and then pass them to the callback
. The callback
has the standard expressjs
-style signature:
callback(err,data)
data
is a JS object composed of two parts:
trial
: Object/Integer. Optional. information about free trials available on all plans.plans
: Array of plan objects. Required. Each object can have as many extraneous properties as it wants, but must have at least:name
: the name of the plan. String.item
: for each item limited, a key in the name of the item limited, and the value of an integer of the limit.If there is no free trial offered, you can simplify the response by just returning the array of plans.
Examples:
No trial, just plans
{plans:[{name:"free",clients:3,groups:2},{name:"bronze",clients:5,groups:10}]}
No trial, just plans (simpler form)
[{name:"free",clients:3,groups:2},{name:"bronze",clients:5,groups:10}]
Free trial and plans
{trial:14,plans:[{name:"free",clients:3,groups:2},{name:"bronze",clients:5,groups:10}]}
In these examples, anyone in the "free" plan can have 3 clients and 2 groups, while anyone in the "bronze" plan can have 5 clients and 10 groups.
If this is the entire set of plans, then the only limits are on creating a new client or group, i.e. POST /.../groups
. What about other actions?
In all cases, a limit value means:
10
): that number and no more0
): none allowed, this action is blocked in this plannull
or undefined
: unlimited number allowedEach plan object in the returned array must have the following properties:
name
: String. Required. The name of the plan. This is never shown to the end-user (unless you choose to do so), but is used to relate the plan a user is on to the limits of the plan.limits
: Object. Optional. The limits for each resource type.Each key of limits
represents an action with limits, e.g. clients
above, and the value should be an object giving the action and limits. Here is an example:
{name:"bronze",clients:{
"index": null
"show":10,
"create": 3,
"update": null
}}
In this example, users in the "bronze" plan can update (with PUT
) or list all of their objects as many times as they want, but are limited to 10 "show" - normally GET /api/clients/:client
- and 3 creations - normally POST /api/clients
.
Because the most common restriction is creating items, there is a shorthand to limit creation:
{name:"bronze", limits:{groups:10}}
Can create no more than 10 groups. All other actions are unrestricted. This is equivalent to:
{name:"bronze",limits:{groups:{
create:10
}}}
But shorter is better, right?
If you have no limits
property, then all properties of the plan itself - except the reserved "name" property - will be treated as limits:
{name:"bronze",groups:{
create:10
}}
or even
{name:"bronze",groups:10}
Of course, for subscriber to know that the request is creating a "clients", it needs to know how to match a path to a "clients". It knows it is a "create" because the request method is POST
, but how does it know if /api/clients
or /api/clients10
or /foo/bar/client/25/me
is the path you POST
to to create a "clients"?
The two configuration options to subscriber - base
and paths
- determine this.
As usual, there is the default, and the explicit.
paths
are given, then the path to each resource is simply base+name
. Since base
defaults to '/', then passing no parameters means that "clients" will be at /clients
and /clients/:client
.base
parameter. For example, an option of {base:'/api'}
means "clients" will be at /api/clients
and /api/clients/:client
.{paths:{clients:'/foo/path/to/clients'}}
which will create paths at /foo/path/to/clients
and /foo/path/to/clients/:client
. Any resource not explicitly listed will fall back to the default.base
; resources with absolute paths will be taken as is; resources with relative paths will be added to the base
.Here are some examples. In all cases, the plan returned is {name:"bronze",clients:10, groups:20}
Will look at the following paths:
GET /clients
GET /clients/:client
PUT /clients/:client
POST /clients
PATCH /clients/:client
DELETE /clients/:client
GET /groups
GET /groups/:group
PUT /groups/:group
POST /groups
PATCH /groups/:group
DELETE /groups/:group
subscriber.init({base:'/api'}); // or '/api/', since subscriber intelligently manages the trailing slash
Will look at the following paths:
GET /api/clients
GET /api/clients/:client
PUT /api/clients/:client
POST /api/clients
PATCH /api/clients/:client
DELETE /api/clients/:client
GET /api/groups
GET /api/groups/:group
PUT /api/groups/:group
POST /api/groups
PATCH /api/groups/:group
DELETE /api/groups/:group
subscriber.init({paths:{
clients: "/foo/path/to/clients"
}});
Note that there is no path given for groups, so it defaults:
GET /foo/path/to/clients
GET /foo/path/to/clients/:client
PUT /foo/path/to/clients/:client
POST /foo/path/to/clients
PATCH /foo/path/to/clients/:client
DELETE /foo/path/to/clients/:client
GET /groups
GET /groups/:group
PUT /groups/:group
POST /groups
PATCH /groups/:group
DELETE /groups/:group
subscriber.init({base:'/api',paths:{
clients: "/my/clients",
groups: "some/groups"
}});
Note that "groups" have a relative path, so base
is prepended, while clients
have an absolute path.
GET /my/clients
GET /my/clients/:client
PUT /my/clients/:client
POST /my/clients
PATCH /my/clients/:client
DELETE /my/clients/:client
GET /api/some/groups
GET /api/some/groups/:group
PUT /api/some/groups/:group
POST /api/some/groups
PATCH /api/some/groups/:group
DELETE /api/some/groups/:group
If a free trial option is provided in plans
, then users can have a free trial of any plan.
The trial option is provide as part of the response to plans()
as follows:
{trial:{duration:14,fallback:"free"},plans:[{name:"free",groups:2},{name:"premium",groups:5},{name:"pro",groups:10}]}
The value of trial is, quite simply, the length in days of the free trial. In the above example, a user can get a free trial of the "premium" or "pro" plan for 14 days, after which they will fall back to the "free" plan.
If you don't want the user to have any fallback - i.e. after the free trial, they get no plan at all - then eliminate the "fallback" property:
{trial:{duration:14},plans:[{name:"premium",groups:5},{name:"pro",groups:10}]}
Which can be short-handed as:
{trial:14,plans:[{name:"premium",groups:5},{name:"pro",groups:10}]}
The other activity for which db
is responsible is telling subscriber
all it needs to know about a user: how many of something it already has.
db.user("john",function(err,res){
// res is, e.g. {name:"john",plan:{name:"free",join:1234567,expire:1238899},usage:{clients:3,groups:2}}
});
subscriber will get
"john", and receive back what usage "john" actually has right now. In our example above, he already has 3 clients and 2 groups.
The "user" object has the following properties:
The "plan" property is an object with the following properties:
true
, then this person is in a free trial. Defaults to false
.Date.getTime()
)Date.getTime()
)If you do not want to track when the user joined the plan, when his current plan expires, and if it is a trial, you can use the following shorthand:
{name:"john",plan:"free",usage:{clients:3,groups:2}}
The "usage" property is an object. Each key is the name of a resource that is protected by subscriber; each value is the integer of the current usage of this user.
{name:"john",plan:"free",usage:{clients:3,groups:2}}
User "john" has 3 resources of type "clients" and 2 of type "groups".
There is also a shorthand version you can use, in which there is no property "usage". In that case, every property of the "user" object will be treated as a resource, except for reserved words "usage", "name", "plan".
{name:"john",plan:"free",clients:3,groups:2}
How does subscriber know which user is logged in, and thus which "name" to pass as the first argument to db.user()
? It looks at req.user
using the following logic:
req.user = "john"
will lead to db.user("john",callback)
req.user.id
, e.g. req.user.id = "john" will lead to
db.user("john",callback)`null
or undefined
, there is no userIf your system offers free trials, then the user
object needs to have additional information about how this user signed up for the trial.
{name:"john",plan:{name:"pro",join:1234567,expire:1238899,trial:true},usage:{clients:3,groups:2}}
When the user is no longer in a free trial, then "trial" should be set to false
or simply deleted.
Let's use a real example. "john" is logged in and wants to create a new client. subscriber sees the request. If plans are already loaded and not timed out, it goes straight to checking its plans in cache, else it loads them again:
db.plans(function(err,res){
// res is:
[
{name:"free",clients:3},
{name:"bronze",clients:5}
]
});
As soon as "john" makes his POST /clients
request, subscriber retrieves all of the information about "john":
db.user("john",function(err,res){
// res is:
{name:"john",plan:"free",clients:3,groups:2}
});
"john" is on the plan named "free", and he has 3 clients. Checking the plans, we see that the "free" plan allows a maximum of 3 clients. "john" has hit his limit, and his request to add another "client" will have a response of 403
.
If the request succeeds, subscriber just calls next()
and passes it on to your next route. subscriber becomes completely transparent.
If the request hits a limit, subscriber will send back a 403
, which is the appropriate error code for a request that is unauthorized.
The body of the response will detail the failure in JSON:
{reason:"subscription",plan:"free",item:"clients",maximum:3}
It gives the reason for the failure (a subscription limit), the plan the user is on, the item that exceeded the limits, and the maximum allowed under the plan.
subscriber is released under the MIT License http://www.opensource.org/licenses/mit-license.php
Avi Deitcher https://github.com/deitch