In 2026, Rootable wants to start on-boarding paying customers. As an under resourced staff, we don't want our capacity to limit how many organizations we can onboard. We want folks to be able to sign up for a 30-day trial or sign up to use the service.
We decided that we wouldn't tackle payments in this change. Programatically handling payments adds a ton of complexity. We don't have lots of users, and the users we expect to sign up are whole orgs, not individual users, so the volume is unlikely to be super high initially.
Tech details
┌────────────┐ ┌──────────────┐
│ trial ├─┬─►│pricing tier 1│
┌─►│subscription│ │ └──────────────┘
┌────────────┐ │ └────────────┘ │
│organization├──┤ ┌────────────┐ │
└────────────┘ │ │ paid ├─┘
└─►│subscription│
└────────────┘
Organizations can now have a pricing tier via a subscription. The subscription has an expiration date and is marked as trial or paid. It points to one of the existing pricing tiers. Pricing tiers can be selectable meaning they show up on the sign up form (or are hidden when selectable: false).
Some ideas behind this model are:
- We can change pricing tiers overtime without invalidating subscriptions. Adding new tiers and hiding previous ones doesn't interrupt usage.
trial or paid allows us to show a "x days remaining on trial" banner 30 days out. Since we offer monthly and yearly subscriptions, we would have no way to differentiate between a paid, monthly subscription and a trial. trial vs paid lets us make that distinction.
- Checking if an org has a valid subscription can be done by seeing if an org have any subscriptions with an
expires_at in the future.
- Checking to see if their trial expires soon can be done by seeing if an org's most-future, expiring subscription has is less than 30 days out.
Actually signing up
The sign up form is found at /sign_up. We don't link to it anywhere, yet. We'll just be passing it out manually.
When organizations sign up, they also choose an email/pass. That's used to create a user that is an organizational admin of the newly created org.
Finally, it sends us an email when someone signs up.
Expiring subscriptions
When an organizations trial or paid subscription runs out, any page they try to visit sends them to a bare page telling them they need to reach out to us to re-subscribe.
Same thing for any volunteers of that org. However, if any of the volunteer's orgs have an expired subscription they will be redirected. Ideally we'd just redirect them if they tried to access data from an organization with the expired subscription and let them access other data as usual, but that's out of scope for now. We also don't expect any volunteers to be part of multiple orgs at the moment.
The way we block people is we've nested all the admin controllers under OrganizationScopeController which checks the organization's subscription and redirects if it's expired. The plain OrganizationsController is also nested here, but it gets the current organization via the id param instead of organization_id. This difference hints to me that the controller should actually be only accessible to operations users and the current show action should be something like OrganizationDashboardController. Out of scope for now.
Default values
One thing I learned while making this is that default parameters need to use a proc if they are dynamic. I ran into this with subscriptions.expires_at because we want it to default to 30.days.from_now.
Default values can be static or dynamic. It's not well documented, but if you use a proc the value is recalculated every time a model is built (if no other value exists).
This is critical when using a date because otherwise whatever value is set when the class is loaded will be used. So the date will be frozen at load time, not run time. So for a default value of a date, that date will remain static even if it's set to something dynamic like 30.days.from_now. Over time, this date will get closer and closer and even move into the past if left long enough.
You can test this by creating new instances of the model in the rails console with the proc and with a static value.
In 2026, Rootable wants to start on-boarding paying customers. As an under resourced staff, we don't want our capacity to limit how many organizations we can onboard. We want folks to be able to sign up for a 30-day trial or sign up to use the service.
We decided that we wouldn't tackle payments in this change. Programatically handling payments adds a ton of complexity. We don't have lots of users, and the users we expect to sign up are whole orgs, not individual users, so the volume is unlikely to be super high initially.
## Tech details
```
┌────────────┐ ┌──────────────┐
│ trial ├─┬─►│pricing tier 1│
┌─►│subscription│ │ └──────────────┘
┌────────────┐ │ └────────────┘ │
│organization├──┤ ┌────────────┐ │
└────────────┘ │ │ paid ├─┘
└─►│subscription│
└────────────┘
```
Organizations can now have a pricing tier via a subscription. The subscription has an expiration date and is marked as `trial` or `paid`. It points to one of the existing pricing tiers. Pricing tiers can be `selectable` meaning they show up on the sign up form (or are hidden when `selectable: false`).
Some ideas behind this model are:
* We can change pricing tiers overtime without invalidating subscriptions. Adding new tiers and hiding previous ones doesn't interrupt usage.
* `trial` or `paid` allows us to show a "x days remaining on trial" banner 30 days out. Since we offer monthly and yearly subscriptions, we would have no way to differentiate between a paid, monthly subscription and a trial. `trial` vs `paid` lets us make that distinction.
* Checking if an org has a valid subscription can be done by seeing if an org have any subscriptions with an `expires_at` in the future.
* Checking to see if their trial expires soon can be done by seeing if an org's most-future, expiring subscription has is less than 30 days out.
## Actually signing up
The sign up form is found at `/sign_up`. We don't link to it anywhere, yet. We'll just be passing it out manually.
When organizations sign up, they also choose an email/pass. That's used to create a user that is an organizational admin of the newly created org.
Finally, it sends us an email when someone signs up.
## Expiring subscriptions
When an organizations trial or paid subscription runs out, any page they try to visit sends them to a bare page telling them they need to reach out to us to re-subscribe.
Same thing for any volunteers of that org. However, if _any_ of the volunteer's orgs have an expired subscription they will be redirected. Ideally we'd just redirect them if they tried to access data from an organization with the expired subscription and let them access other data as usual, but that's out of scope for now. We also don't expect any volunteers to be part of multiple orgs at the moment.
The way we block people is we've nested all the admin controllers under `OrganizationScopeController` which checks the organization's subscription and redirects if it's expired. The plain `OrganizationsController` is also nested here, but it gets the current organization via the `id` param instead of `organization_id`. This difference hints to me that the controller should actually be only accessible to operations users and the current `show` action should be something like `OrganizationDashboardController`. Out of scope for now.
## Default values
One thing I learned while making this is that default parameters need to use a proc if they are dynamic. I ran into this with `subscriptions.expires_at` because we want it to default to `30.days.from_now`.
Default values can be static or dynamic. It's not well documented, but if you use a proc the value is recalculated every time a model is built (if no other value exists).
This is critical when using a date because otherwise whatever value is set when the class is loaded will be used. So the date will be frozen at load time, not run time. So for a default value of a date, that date will remain static even if it's set to something dynamic like 30.days.from_now. Over time, this date will get closer and closer and even move into the past if left long enough.
You can test this by creating new instances of the model in the rails console with the proc and with a static value.