Skip to content

Commit

Permalink
Update pricing plans (#308)
Browse files Browse the repository at this point in the history
  • Loading branch information
loicknuchel authored Jun 30, 2024
1 parent a513675 commit 614ef33
Show file tree
Hide file tree
Showing 141 changed files with 2,869 additions and 3,431 deletions.
9 changes: 8 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ export FILE_STORAGE_ADAPTER=local
# export SMTP_PASSWORD=
# export SMTP_PORT=

# export SUPPORT_EMAIL=
# export SENDER_EMAIL=
# export CONTACT_EMAIL=
# export SUPPORT_EMAIL=
# export ENTERPRISE_SUPPORT_EMAIL=

# Key features

Expand Down Expand Up @@ -121,6 +123,11 @@ export AUTH_PASSWORD=true
# export STRIPE=true
# export STRIPE_API_KEY=sk_test_abcdef
# export STRIPE_WEBHOOK_SIGNING_SECRET=whsec_1234
# export STRIPE_PRICE_SOLO_MONTHLY=
# export STRIPE_PRICE_SOLO_YEARLY=
# export STRIPE_PRICE_TEAM_MONTHLY=
# export STRIPE_PRICE_TEAM_YEARLY=
# export STRIPE_PRODUCT_ENTERPRISE=
# export STRIPE_PRICE_PRO_MONTHLY=

## Clever Cloud Addon
Expand Down
15 changes: 11 additions & 4 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ These are the basic variables you will **need** to set up Azimutt:
- `PHX_HOST` (required): host of the deployed website (ex: `localhost` or `azimutt.app`), it's used to build absolute urls
- `PORT` (required): the port the server will listen to (ex: `4000`)
- `SECRET_KEY_BASE` (required): the secret used for server encryption (cookies and others), should be at least 64 bytes and you probably want a random value for it
- `LICENCE_KEY` (optional): the licence key to unlock the pro features, contact us if you need one ([email protected])
- `LICENCE_KEY` (optional): the licence key to unlock paid features, contact us if you need one ([email protected])
- `DATABASE_URL` (required): the whole url to connect to your PostgreSQL database (ex: `postgresql://<user>:<pass>@<host>:<port>/<database>`)
- `DATABASE_IPV6` (optional): if `true`, the database driver will use IPV6
- `DATABASE_POOL_SIZE` (optional, default: `10`): the database connection pool size
Expand All @@ -154,8 +154,10 @@ These are the basic variables you will **need** to set up Azimutt:
- `SMTP_USERNAME` (required)
- `SMTP_PASSWORD` (required)
- `SMTP_PORT` (required)
- `SUPPORT_EMAIL` (optional, default `[email protected]`): email shown in Azimutt when users need support
- `SENDER_EMAIL` (optional, default `[email protected]`): email Azimutt will us to send emails
- `CONTACT_EMAIL` (optional, default `[email protected]`): email shown in Azimutt to reach out
- `SUPPORT_EMAIL` (optional, default `[email protected]`): email shown in Azimutt when users need support
- `ENTERPRISE_SUPPORT_EMAIL` (optional, default `[email protected]`): email shown in Azimutt for high priority support


### Key features
Expand All @@ -175,7 +177,7 @@ At least one of authentication methods should be defined:
- `SKIP_EMAIL_CONFIRMATION` (optional): if `true`, users will not be asked to confirm their email (either blocked or soft)
- `REQUIRE_EMAIL_CONFIRMATION` (optional): if `true`, users will not be allowed to use Azimutt until they confirm their email, otherwise they will have a soft confirmation banner
- `REQUIRE_EMAIL_ENDS_WITH` (optional): force all users to use an email ending with a suffix, your domain name for example
- `ORGANIZATION_DEFAULT_PLAN` (optional, values: `free` or `pro`): define the plan an organization has by default when created
- `ORGANIZATION_DEFAULT_PLAN` (optional, values: `free`, `solo`, `team`, `enterprise` or `pro`): define the plan an organization has by default when created
- `GLOBAL_ORGANIZATION` (optional): an organization id, if set, all new users will be added to this organization
- `GLOBAL_ORGANIZATION_ALONE` (optional): if `true`, only the global organization is shown (allows to work like a mono-tenant app)
- `RECAPTCHA` (optional): if `true`, add [reCAPTCHA](https://www.google.com/recaptcha) on register and login
Expand Down Expand Up @@ -207,7 +209,12 @@ At least one of authentication methods should be defined:
- `STRIPE` (optional): if `true`, allow to purchase plans with [Stripe](https://stripe.com), you probably don't need it ^^
- `STRIPE_API_KEY` (required): Stripe api key (ex: `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`)
- `STRIPE_WEBHOOK_SIGNING_SECRET` (required): Stripe webhook secret (ex: `whsec_ayZAyKqOLy34UKNeI3eq4icXVWJam0IW`)
- `STRIPE_PRICE_PRO_MONTHLY` (required): the Stripe price for the pro plan (ex: `price_uJINukB78aAbajUQHy6Ra523`)
- `STRIPE_PRICE_SOLO_MONTHLY` (required): Stripe price for the monthly solo plan (ex: `price_uJINukB78aAbajUQHy6Ra523`)
- `STRIPE_PRICE_SOLO_YEARLY` (required): Stripe price for the yearly solo plan (ex: `price_uJINukB78aAbajUQHy6Ra523`)
- `STRIPE_PRICE_TEAM_MONTHLY` (required): Stripe price for the monthly team plan (ex: `price_uJINukB78aAbajUQHy6Ra523`)
- `STRIPE_PRICE_TEAM_YEARLY` (required): Stripe price for the yearly team plan (ex: `price_uJINukB78aAbajUQHy6Ra523`)
- `STRIPE_PRODUCT_ENTERPRISE` (required): Stripe product for enterprise plan (ex: `prod_eBlQLUZPVprdAo`)
- `STRIPE_PRICE_PRO_MONTHLY` (required): Stripe price for the monthly legacy pro plan (ex: `price_uJINukB78aAbajUQHy6Ra523`)
- `CLEVER_CLOUD` (optional): if `true`, enable auth & hooks for [Clever Cloud Add-on](https://www.clever-cloud.com/doc/extend/add-ons-api)
- `CLEVER_CLOUD_ADDON_ID` (required)
- `CLEVER_CLOUD_PASSWORD` (required)
Expand Down
11 changes: 6 additions & 5 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,18 @@
"value": "4000"
},
"SECRET_KEY_BASE": {
"description": "Secret for encryption of cookies",
"generator": "secret"
},
"FILE_STORAGE_ADAPTER": "s3",
"AUTH_PASSWORD": {
"description": "Enable password-based authentication",
"value": "true"
},
"DATABASE_ENABLE_SSL": {
"description": "Enable SSL for database connections",
"value": "true"
},
"AUTH_PASSWORD": {
"description": "Enable password-based authentication",
"value": "true"
},
"SKIP_ONBOARDING_FUNNEL": {
"description": "Skip the onboarding funnel for quicker testing",
"value": "true"
Expand All @@ -51,7 +52,7 @@
"value": "mailgun"
},
"MAILGUN_BASE_URL": {
"description": "Mailgun base url",
"description": "Mailgun base url",
"value": "https://api.eu.mailgun.net/v3"
}
}
Expand Down
7 changes: 7 additions & 0 deletions backend/assets/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ module.exports = {

],
safelist: [
'w-1/4',
'w-1/5',
'grid-cols-4',
'grid-cols-5',
'lg:grid-cols-3',
'lg:grid-cols-4',
'lg:grid-cols-5',
{
pattern: /(bg|text|border|from|via|to)-(red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-.*/,
},
Expand Down
17 changes: 2 additions & 15 deletions backend/config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,10 @@ config :azimutt,
azimutt_github: "https://github.com/azimuttapp/azimutt",
azimutt_github_issues: "https://github.com/azimuttapp/azimutt/issues",
azimutt_github_issues_new: "https://github.com/azimuttapp/azimutt/issues/new",
pro_plan_seat_price: 13,
free_plan_seats: 2,
free_plan_projects: 2,
# MUST stay in sync with frontend/src/Conf.elm (`features`)
free_plan_layouts: 2,
free_plan_layout_tables: 10,
free_plan_memos: 3,
free_plan_groups: 1,
free_plan_colors: false,
free_plan_local_save: false,
free_plan_private_links: false,
free_plan_sql_export: false,
free_plan_db_analysis: false,
free_plan_db_access: false,
environment: config_env(),
# TODO: find an automated process to build it
version: "2.0.1",
version: "2.1.0",
version_date: "2024-07-01T00:00:00.000Z",
commit_hash: System.cmd("git", ["log", "-1", "--pretty=format:%h"]) |> elem(0) |> String.trim(),
commit_message: System.cmd("git", ["log", "-1", "--pretty=format:%s"]) |> elem(0) |> String.trim(),
commit_date: System.cmd("git", ["log", "-1", "--pretty=format:%aI"]) |> elem(0) |> String.trim(),
Expand Down
11 changes: 9 additions & 2 deletions backend/config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ config :azimutt,
organization_default_plan: System.get_env("ORGANIZATION_DEFAULT_PLAN"),
global_organization: global_organization,
global_organization_alone: global_organization && System.get_env("GLOBAL_ORGANIZATION_ALONE") == "true",
support_email: System.get_env("SUPPORT_EMAIL") || "[email protected]",
sender_email: System.get_env("SENDER_EMAIL") || "[email protected]",
sender_email: System.get_env("SENDER_EMAIL") || Azimutt.config(:azimutt_email),
contact_email: System.get_env("CONTACT_EMAIL") || Azimutt.config(:azimutt_email),
support_email: System.get_env("SUPPORT_EMAIL") || System.get_env("CONTACT_EMAIL") || Azimutt.config(:azimutt_email),
enterprise_support_email: System.get_env("ENTERPRISE_SUPPORT_EMAIL") || System.get_env("SUPPORT_EMAIL") || System.get_env("CONTACT_EMAIL") || Azimutt.config(:azimutt_email),
server_started: DateTime.utc_now()

config :azimutt, Azimutt.Repo,
Expand Down Expand Up @@ -270,6 +272,11 @@ if System.get_env("STRIPE") == "true" do

config :azimutt,
stripe: true,
stripe_price_solo_monthly: System.fetch_env!("STRIPE_PRICE_SOLO_MONTHLY"),
stripe_price_solo_yearly: System.fetch_env!("STRIPE_PRICE_SOLO_YEARLY"),
stripe_price_team_monthly: System.fetch_env!("STRIPE_PRICE_TEAM_MONTHLY"),
stripe_price_team_yearly: System.fetch_env!("STRIPE_PRICE_TEAM_YEARLY"),
stripe_product_enterprise: System.fetch_env!("STRIPE_PRODUCT_ENTERPRISE"),
stripe_price_pro_monthly: System.fetch_env!("STRIPE_PRICE_PRO_MONTHLY")

config :stripity_stripe,
Expand Down
160 changes: 115 additions & 45 deletions backend/lib/azimutt.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,60 +28,130 @@ defmodule Azimutt do
end

def plans do
# Next ones: Explore ($3), Expand ($13), Extend ($25)
[
%{
# MUST match Stripe prices defined in env vars
%{
free: %{
id: :free,
name: "Explorer",
description: "Design or Explore any kind of database, seamlessly.",
monthly: 0,
annually: 0,
name: "Free",
description: "Quickly explore your db with one command. No long term save.",
monthly: "Free",
yearly: "Free",
features: [
"Schema & Data exploration",
"Database design with AML",
"Unlimited Tables",
"2 projects with 2 layouts of 10 tables",
"2 collaborators"
],
cta: "Select this plan",
buy: "/login?plan=free",
selected: false
"Unlimited tables",
"Schema exploration",
"Data exploration"
]
},
%{
id: :pro,
name: "Pro",
description: "Remove limits, make Azimutt a central space for collaboration.",
monthly: 13,
annually: 130,
solo: %{
id: :solo,
name: "Solo",
description: "Personal usage with one project. Allows design and custom colors.",
monthly: 9,
yearly: 7,
unit: "€ / month",
features: [
"Everything included in Explorer",
"Unlimited projects",
"Unlimited layouts",
"Unlimited notes & memos",
"Full database analysis",
"Premium support"
],
cta: "Try this plan",
buy: "/login?plan=pro",
selected: true
"Free plan features",
"Long term usage",
"Database design",
"Schema export"
]
},
%{
team: %{
id: :team,
name: "Team",
description: "Collaborate on Azimutt with all database features.",
monthly: 42,
yearly: 35,
unit: "€ / user / month",
features: [
"Solo plan features",
"Database analysis",
"Collaboration",
"Documentation",
"AI capabilities",
"Export project"
]
},
enterprise: %{
id: :enterprise,
name: "Enterprise",
description: "Features you only dreamed of to ease database understanding and management.",
monthly: nil,
annually: nil,
description: "Getting serious: higher limits, security, control and automation.",
monthly: "Custom",
yearly: "Custom",
features: [
"Everything included in Pro",
"User roles",
"Schema change alerting",
"Advanced data access",
"AI query generation"
],
cta: "Contact us",
buy: "mailto:#{Azimutt.config(:support_email)}",
selected: false
"Team plan features",
"Unlimited usage",
"User management",
"Custom integrations"
]
},
pro: %{
id: :pro,
name: "Pro",
monthly: 13,
yearly: 13,
features: []
}
}
end

def active_plans, do: [plans().free, plans().solo, plans().team, plans().enterprise]

# MUST stay in sync with frontend/src/Conf.elm (`features`)
def limits do
%{
# Database features
schema_exploration: %{name: "Schema exploration", free: true, solo: true, team: true, enterprise: true, pro: true},
data_exploration: %{name: "Data exploration", free: true, solo: true, team: true, enterprise: true, pro: true},
colors: %{name: "Custom colors", free: false, solo: true, team: true, enterprise: true, pro: true},
# TODO: rename `aml` to `db_design`
aml: %{name: "Database design (AML)", free: false, solo: true, team: true, enterprise: true, pro: true},
# saved_queries: %{name: "Saved queries", free: false, solo: false, team: false, enterprise: true, pro: true, description: "Soon... Save and share useful queries."},
# dashboard: %{name: "Dashboard", free: false, solo: false, team: false, enterprise: true, pro: true, description: "Soon... Visually see query results."},
# db_stat_history: %{name: "Stats history", free: false, solo: false, team: false, enterprise: true, pro: true, description: "Soon... Keep evolutions of database stats."},
schema_export: %{name: "Export schema", free: false, solo: true, team: true, enterprise: true, pro: true, description: "Export your schema as SQL, AML or JSON."},
ai: %{name: "AI features", free: false, solo: false, team: true, enterprise: true, pro: true},
analysis: %{
name: "Database analysis",
free: "preview",
solo: "preview",
team: "snapshot",
enterprise: "trends",
pro: "trends",
description: "preview: top 3 suggestions, snapshot: all suggestions, trends: more suggestions based on evolution"
},
project_export: %{name: "Export project", free: false, solo: false, team: true, enterprise: true, pro: true},
# Product quotas
users: %{name: "Max users", free: 1, solo: 1, team: 5, enterprise: nil, pro: nil},
projects: %{name: "Max projects", free: 0, solo: 1, team: 5, enterprise: nil, pro: nil, description: "0 means you can create a project but can't save it."},
project_dbs: %{name: "Max db/project", free: 1, solo: 1, team: 3, enterprise: nil, pro: nil},
project_layouts: %{name: "Max layout/project", free: 1, solo: 3, team: 20, enterprise: nil, pro: nil},
layout_tables: %{name: "Max table/layout", free: 10, solo: 10, team: 40, enterprise: nil, pro: nil},
project_doc: %{name: "Max doc/project", free: 10, solo: 10, team: 1000, enterprise: nil, pro: nil},
# Extended integration
project_share: %{name: "Sharing project", free: false, solo: false, team: false, enterprise: true, pro: true, description: "Use private links & embed to share with guest."},
api: %{name: "API access", free: false, solo: false, team: false, enterprise: true, pro: true, description: "Fetch and update sources and documentation programmatically."},
sso: %{name: "SSO", free: false, solo: false, team: false, enterprise: true, pro: false, description: "Soon..."},
user_rights: %{name: "User rights", free: false, solo: false, team: false, enterprise: true, pro: false, description: "Soon... Have read-only users in your organization."},
gateway_custom: %{name: "Custom gateway", free: false, solo: false, team: false, enterprise: true, pro: false, description: "Soon... Securely connect to your databases."},
billing: %{name: "Flexible billing", free: false, solo: false, team: false, enterprise: true, pro: false},
support_on_premise: %{name: "On-premise support", free: false, solo: false, team: false, enterprise: true, pro: false},
support_enterprise: %{name: "Enterprise support", free: false, solo: false, team: false, enterprise: true, pro: false, description: "Priority email, answer within 48h."},
consulting: %{name: "1h expert consulting", free: false, solo: false, team: false, enterprise: true, pro: false},
roadmap: %{name: "Roadmap impact", free: "suggestions", solo: "suggestions", team: "suggestions", enterprise: "discussions", pro: "suggestions"}
}
end

# MUST stay sync with backend/lib/azimutt_web/templates/partials/_streak.html.heex
def streak do
[
%{goal: 4, feature: :colors, limit: true},
%{goal: 6, feature: :aml, limit: true},
%{goal: 10, feature: :ai, limit: true},
%{goal: 15, feature: :project_layouts, limit: nil},
%{goal: 25, feature: :schema_export, limit: true},
%{goal: 40, feature: :analysis, limit: "trends"},
%{goal: 60, feature: :project_share, limit: true}
]
end

Expand Down
2 changes: 1 addition & 1 deletion backend/lib/azimutt/admin.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ defmodule Azimutt.Admin do
def count_organizations, do: Organization |> Repo.aggregate(:count, :id)
def count_personal_organizations, do: Organization |> where([o], o.is_personal == true) |> Repo.aggregate(:count, :id)
def count_non_personal_organizations, do: Organization |> where([o], o.is_personal == false) |> Repo.aggregate(:count, :id)
def count_stripe_subscriptions, do: Organization |> where([o], not is_nil(o.stripe_subscription_id)) |> Repo.aggregate(:count, :id)
def count_paid_organizations, do: Organization |> where([o], not is_nil(o.plan) and o.plan != "free") |> Repo.aggregate(:count, :id)
def count_clever_cloud_resources, do: CleverCloud.Resource |> Repo.aggregate(:count, :id)
def count_heroku_resources, do: Heroku.Resource |> Repo.aggregate(:count, :id)
def count_projects, do: Project |> Repo.aggregate(:count, :id)
Expand Down
21 changes: 5 additions & 16 deletions backend/lib/azimutt/clever_cloud.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,6 @@ defmodule Azimutt.CleverCloud do
def app_addons_url, do: "#TODO"
def app_settings_url, do: "#TODO"

def allowed_members(plan) do
team_members = Regex.named_captures(~r/pro-(?<members>[0-9]+)/, plan)

if team_members do
String.to_integer(team_members["members"])
else
Azimutt.config(:free_plan_seats)
end
end

# use only for CleverCloudController.index local helper
def all_resources do
Resource
Expand Down Expand Up @@ -63,22 +53,21 @@ defmodule Azimutt.CleverCloud do
end
end

def add_member_if_needed(%Resource{} = resource, %Organization{} = organization, %User{} = current_user) do
slots_in_plan = allowed_members(resource.plan)
def add_member_if_needed(%Organization{} = organization, %User{} = current_user) do
existing_members = Organizations.count_member(organization)

cond do
existing_members > slots_in_plan ->
{:error, :too_many_members}

Organizations.has_member?(organization, current_user) ->
{:ok, :already_member}

existing_members < slots_in_plan ->
existing_members < organization.plan_seats ->
OrganizationMember.new_member_changeset(organization.id, current_user)
|> Repo.insert()
|> Result.map(fn _ -> :member_added end)

existing_members > organization.plan_seats ->
{:error, :too_many_members}

true ->
{:error, :member_limit_reached}
end
Expand Down
Loading

0 comments on commit 614ef33

Please sign in to comment.