Rails app blueprint
Jumpstart Pro
Add Avo 3 to your Jumpstart Pro app blazing fast!
We are heavily used Avo Pro in order to build a new "admin first" platform that can be managed by our non-tech team with ease.
The interface is extremely intuitive and can be extended fast with custom actions.

Paul Werther
CTO, greenhats GmbH
Run the following command to quickly apply this app blueprint
If you think this blueprint needs changes please submit a PR here.
Full blueprint
# Blueprint info:
# Link: https://avohq.io/blueprints/jumpstart-pro
# Repository: https://github.com/avo-hq/jumpstart-pro-app-template
# List of all Avo files with path and contents
files = {"app/avo/cards/active_subscriptions.rb"=>"class Avo::Cards::ActiveSubscriptions < Avo::Cards::MetricCard\n self.id = \"active_subscriptions\"\n self.label = \"Active subscriptions\"\n self.description = \"Total number of active subscriptions\"\n\n def query\n result ::Pay::Subscription.active.count\n end\nend\n", "app/avo/cards/total_revenue.rb"=>"class Avo::Cards::TotalRevenue < Avo::Cards::MetricCard\n self.id = \"total_revenue\"\n self.label = \"Total revenue\"\n self.prefix = \"$\"\n\n def query\n case arguments[:period]\n when :last_12_months\n result last_12_months\n when :last_month\n result last_month\n when :this_month\n result this_month\n else\n result total_revenue\n end\n end\n\n def total_revenue\n revenue_in_cents = ::Pay::Charge.sum(:amount)\n refunds_in_cents = ::Pay::Charge.sum(:amount_refunded)\n (revenue_in_cents - refunds_in_cents) / 100.0\n end\n\n def last_12_months\n revenue_for_range 12.months.ago..Time.current\n end\n\n def last_month\n month = Time.current.prev_month\n revenue_for_range month.beginning_of_month..month.end_of_month\n end\n\n def this_month\n month = Time.current\n revenue_for_range month.beginning_of_month..month.end_of_month\n end\n\n def revenue_for_range(range)\n revenue_in_cents = ::Pay::Charge.where(created_at: range).sum(:amount)\n refunds_in_cents = ::Pay::Charge.where(created_at: range).sum(:amount_refunded)\n (revenue_in_cents - refunds_in_cents) / 100.0\n end\nend\n", "app/avo/cards/users_count.rb"=>"class Avo::Cards::UsersCount < Avo::Cards::MetricCard\n self.id = \"users_count\"\n self.label = \"Users count\"\n self.description = \"Total number of users registered\"\n\n def query\n result ::User.all.count\n end\nend\n", "app/avo/dashboards/overview.rb"=>"class Avo::Dashboards::Overview < Avo::Dashboards::BaseDashboard\n self.id = \"overview\"\n self.name = \"Overview\"\n self.grid_cols = 4\n\n def cards\n divider label: \"Revenue\"\n card Avo::Cards::TotalRevenue,\n arguments: {\n period: :this_month\n },\n label: \"This month\"\n card Avo::Cards::TotalRevenue,\n arguments: {\n period: :last_month\n },\n label: \"Last month\"\n card Avo::Cards::TotalRevenue,\n arguments: {\n period: :last_12_months\n },\n label: \"Last 12 months\"\n card Avo::Cards::TotalRevenue\n\n divider\n\n card Avo::Cards::UsersCount\n card Avo::Cards::ActiveSubscriptions\n end\nend\n", "app/avo/resources/account.rb"=>"class Avo::Resources::Account < Avo::BaseResource\n self.title = :name\n self.includes = [:owner, :users]\n self.search = {\n query: -> { query.ransack(id_eq: params[:q], name_cont: params[:q], m: \"or\").result(distinct: false) },\n item: -> {\n {\n title: record.name,\n image_url: record.avatar\n }\n }\n }\n\n def fields\n main_panel do\n field :id, as: :id\n field :avatar,\n as: :file,\n is_image: true,\n link_to_resource: true,\n rounded: true,\n hide_on: :forms\n field :owner, as: :belongs_to\n field :name, as: :text\n field :personal, as: :boolean\n field :users_count, as: :text do\n record.users.length\n end\n\n sidebar do\n field :extra_billing_info, as: :textarea\n field :created_at, as: :date_time\n field :updated_at, as: :date_time\n end\n end\n\n tabs do\n field :subscriptions, as: :has_many\n field :pay_customers, as: :has_many\n field :charges, as: :has_many\n field :account_users, as: :has_many\n field :users, as: :has_many\n end\n end\nend\n", "app/avo/resources/account_user.rb"=>"class Avo::Resources::AccountUser < Avo::BaseResource\n self.title = :id\n self.includes = []\n\n def fields\n main_panel do\n field :id, as: :id\n field :account, as: :belongs_to\n field :user, as: :belongs_to\n field :roles,\n as: :boolean_group,\n options: {\n admin: \"Administrator\",\n member: \"Member\",\n }\n field :admin?, as: :boolean do\n record.roles[\"admin\"] == true\n end\n\n sidebar do\n field :created_at, as: :date_time\n field :updated_at, as: :date_time\n end\n end\n end\nend\n", "app/avo/resources/announcement.rb"=>"class Avo::Resources::Announcement < Avo::BaseResource\n self.title = :title\n self.includes = []\n self.search = {\n query: -> { query.ransack(id_eq: params[:q], title_cont: params[:q], m: \"or\").result(distinct: false) },\n item: -> {\n {\n title: record.title,\n }\n }\n }\n\n def fields\n field :id, as: :id\n field :title, as: :text\n field :description, as: :trix, always_show: true\n field :kind, as: :badge, options: {success: \"new\", info: \"improvement\", danger: \"fix\", warning: \"update\"}, hide_on: :forms\n field :kind, as: :select, options: Announcement::TYPES.map { |t| [t, t] }.to_h, show_on: :forms\n field :published_at, as: :date_time\n end\nend\n", "app/avo/resources/charge.rb"=>"class Avo::Resources::Charge < Avo::BaseResource\n self.title = :processor_id\n self.includes = []\n self.model_class = Pay::Charge\n self.search = {\n query: -> { query.ransack(id_eq: params[:q], processor_id_cont: params[:q], m: \"or\").result(distinct: false)},\n item: -> {\n {\n title: record.processor_id\n }\n }\n }\n\n def fields\n field :id, as: :id\n field :customer, as: :belongs_to\n field :amount, as: :text\n field :amount_refunded, as: :text\n field :created_at, as: :date_time\n\n with_options only_on: :show do\n field :processor_id, as: :text, format_using: -> do\n link_to value, view_context.controller.charge_processor_url(model), target: :_blank\n rescue\n value\n end\n field :subscription, as: :belongs_to\n field :updated_at, as: :date_time\n end\n end\nend\n", "app/avo/resources/connected_account.rb"=>"class Avo::Resources::ConnectedAccount < Avo::BaseResource\n self.title = :id\n self.includes = []\n self.model_class = ConnectedAccount\n\n def fields\n field :id, as: :id\n field :user, as: :belongs_to\n field :image_url, as: :external_image\n field :expired?, as: :boolean, hide_on: :forms\n end\nend\n", "app/avo/resources/customer.rb"=>"class Avo::Resources::Customer < Avo::BaseResource\n self.title = :name\n self.includes = [:owner]\n self.search = {\n query: -> { query.ransack(id_eq: params[:q], processor_id_cont: params[:q], m: \"or\").result(distinct: false) },\n item: -> {\n {\n title: record.name,\n }\n }\n }\n self.model_class = Pay::Customer\n\n def fields\n main_panel do\n field :id, as: :id\n field :owner, as: :belongs_to, polymorphic_as: :owner, types: [::Account]\n\n field :processor, as: :badge, options: {success: \"stripe\"}\n field :processor_id, as: :text, format_using: -> do\n link_to(value, view_context.controller.customer_processor_url(model), target: :_blank)\n rescue\n value\n end\n field :default, as: :boolean\n field :data, as: :key_value\n\n sidebar do\n field :created_at, as: :date_time\n field :updated_at, as: :date_time\n field :deleted_at, as: :date_time\n end\n end\n\n tabs do\n field :subscriptions, as: :has_many\n field :charges, as: :has_many\n end\n end\nend\n", "app/avo/resources/payment_method.rb"=>"class Avo::Resources::PaymentMethod < Avo::BaseResource\n self.includes = []\n self.model_class = Pay::PaymentMethod\n self.search = {\n query: -> { query.ransack(id_eq: params[:q], processor_id_cont: params[:q], m: \"or\").result(distinct: false) },\n item: -> {\n {\n title: record.id,\n }\n }\n }\n\n def fields\n main_panel do\n field :id, as: :id\n field :customer, as: :belongs_to\n field :processor_id, as: :text\n field :type, as: :text\n field :default, as: :boolean\n field :data, as: :key_value\n\n sidebar do\n field :extra_billing_info, as: :key_value\n field :created_at, as: :date_time\n field :updated_at, as: :date_time\n end\n end\n end\nend\n", "app/avo/resources/plan.rb"=>"class Avo::Resources::Plan < Avo::BaseResource\n self.title = :name\n self.includes = []\n self.search = {\n query: -> { query.ransack(id_eq: params[:q], name_cont: params[:q], description_cont: params[:q], m: \"or\").result(distinct: false) },\n item: -> {\n {\n title: record.name,\n }\n }\n }\n\n def fields\n main_panel do\n field :id, as: :id\n field :name, as: :text\n field :description, as: :text, hide_on: :index\n field :hidden, as: :boolean\n field :amount, as: :number, help: \"Price in cents\", format_using: -> do\n if view == :edit || view == :new\n value\n else\n (BigDecimal(value.to_s) / 100).round(2)\n end\n end\n field :currency, as: :select, options: Pay::Currency.all.map { |iso, v| [\"\#{iso.upcase} - \#{v[\"name\"]}\", iso] }.to_h\n field :interval, as: :select, options: [:month, :year]\n field :interval_count, as: :number\n field :trial_period_days, as: :number\n\n field :features, as: :tags\n\n sidebar do\n field :created_at, as: :date_time\n field :updated_at, as: :date_time\n field :processor_details, as: :heading, label: \"Processor details\"\n field :stripe_id, as: :text, format_using: -> do\n if view == :edit || view == :new\n value\n else\n link_to value, \"https://dashboard.stripe.com/plans/\#{value}\", target: :_blank\n end\n rescue\n value\n end\n field :braintree_id, as: :text, format_using: -> do\n if view == :edit || view == :new\n value\n else\n link_to value, \"https://sandbox.braintreegateway.com/merchants/\#{Pay::Braintree.merchant_id}/plans/\#{id}\", target: :_blank\n end\n rescue\n value\n end\n field :paddle_id, as: :text\n field :fake_processor_id, as: :text\n end\n end\n end\nend\n", "app/avo/resources/subscription.rb"=>"class Avo::Resources::Subscription < Avo::BaseResource\n self.title = :processor_id\n self.includes = [:owner, :customer]\n self.search = {\n query: -> { query.ransack(id_eq: params[:q], name_cont: params[:q], processor_id_cont: params[:q], m: \"or\").result(distinct: false) },\n item: -> {\n {\n title: record.processor_id,\n }\n }\n }\n self.model_class = Pay::Subscription\n\n def fields\n main_panel do\n field :id, as: :id\n with_options only_on: :index do\n field :customer, as: :belongs_to\n field :name, as: :text\n field :active?, as: :boolean, name: \"Active\"\n field :cancelled?, as: :boolean, name: \"Cancelled\"\n end\n field :owner, as: :has_one\n field :processor_id, as: :text, format_using: ->(id) do\n link_to id, view_context.controller.subscription_processor_url(model), target: :_blank\n rescue\n id\n end\n field :active?, as: :boolean\n field :processor, as: :badge, options: {success: \"Stripe\"} do |model, *args|\n name = record.payment_processor.class.to_s.split(\"::\").second\n if name.downcase.include? \"fake\"\n \"Fake\"\n else\n name\n end\n rescue\n \"Unknown\"\n end\n\n sidebar do\n field :status, as: :status, readonly: true, failed_when: [:canceled], loading_when: [:trialing]\n field :trial_ends_at_ago, as: :text\n field :created_at, as: :date_time, readonly: true\n field :ends_at, as: :date_time, readonly: true\n field :processor_id, as: :text, readonly: true, as_html: true do\n case record.customer.processor\n when \"stripe\"\n link_to(record.processor_id, \"https://dashboard.stripe.com/subscriptions/\#{record.processor_id}\", target: :_blank)\n else\n record.processor_id\n end\n end\n field :processor_plan, as: :text, readonly: true\n field :quantity, as: :number, readonly: true\n end\n end\n end\nend\n", "app/avo/resources/user.rb"=>"class Avo::Resources::User < Avo::BaseResource\n self.title = :name\n self.includes = [:accounts]\n self.search = {\n query: -> { query.ransack(id_eq: params[:q], name_cont: params[:q], email_cont: params[:q], m: \"or\").result(distinct: false) },\n item: -> {\n {\n title: record.name,\n }\n }\n }\n\n def fields\n field :id, as: :id\n field :avatar, as: :file, is_image: true, hide_on: :forms, link_to_resource: true\n field :name, as: :text\n field :email, as: :text\n field :admin, as: :boolean, help: \"Whether the user has admin capabilities.\"\n\n with_options hide_on: :forms do\n field :number_of_accounts, as: :text do\n record.accounts.length\n end\n end\n\n tabs do\n field :accounts, as: :has_many\n field :connected_accounts, as: :has_many\n end\n\n tabs do\n tab \"General\" do\n panel do\n field :time_zone, as: :text\n field :invitations_count, as: :text\n end\n end\n tab \"Account management\" do\n panel do\n field :password, as: :password\n field :password_confirmation, as: :password\n field :reset_password_token, as: :password\n field :reset_password_sent_at, as: :date_time\n field :remember_created_at, as: :date_time\n field :confirmation_token, as: :text\n field :confirmed_at, as: :date_time\n field :confirmation_sent_at, as: :date_time\n field :unconfirmed_email, as: :text\n field :created_at, as: :date_time\n field :updated_at, as: :date_time\n field :invitation_token, as: :text\n field :invitation_created_at, as: :date_time\n field :invitation_sent_at, as: :date_time\n field :invitation_accepted_at, as: :date_time\n field :invitation_limit, as: :number\n field :terms_of_service, as: :boolean\n field :accepted_terms_at, as: :date_time\n field :accepted_privacy_at, as: :date_time\n end\n end\n end\n end\nend\n", "app/controllers/avo/account_users_controller.rb"=>"# This controller has been generated to enable Rails' resource routes.\n# You shouldn't need to modify it in order to use Avo.\nclass Avo::AccountUsersController < Avo::ResourcesController\nend\n", "app/controllers/avo/accounts_controller.rb"=>"class Avo::AccountsController < Avo::ResourcesController\nend\n", "app/controllers/avo/announcements_controller.rb"=>"class Avo::AnnouncementsController < Avo::ResourcesController\nend\n", "app/controllers/avo/charges_controller.rb"=>"# This controller has been generated to enable Rails' resource routes.\n# You shouldn't need to modify it in order to use Avo.\nclass Avo::ChargesController < Avo::ResourcesController\n include Jumpstart::AdministrateHelpers\nend\n", "app/controllers/avo/connected_accounts_controller.rb"=>"# This controller has been generated to enable Rails' resource routes.\n# You shouldn't need to modify it in order to use Avo.\nclass Avo::ConnectedAccountsController < Avo::ResourcesController\nend\n", "app/controllers/avo/customers_controller.rb"=>"class Avo::CustomersController < Avo::ResourcesController\n include Jumpstart::AdministrateHelpers\nend\n", "app/controllers/avo/payment_methods_controller.rb"=>"# This controller has been generated to enable Rails' resource routes.\n# You shouldn't need to modify it in order to use Avo.\nclass Avo::PaymentMethodsController < Avo::ResourcesController\nend\n", "app/controllers/avo/plans_controller.rb"=>"class Avo::PlansController < Avo::ResourcesController\n include Jumpstart::AdministrateHelpers\nend\n", "app/controllers/avo/subscriptions_controller.rb"=>"class Avo::SubscriptionsController < Avo::ResourcesController\n include Jumpstart::AdministrateHelpers\nend\n", "app/controllers/avo/users_controller.rb"=>"class Avo::UsersController < Avo::ResourcesController\nend\n", "config/initializers/avo.rb"=>"# For more information regarding these settings check out our docs https://docs.avohq.io\nAvo.configure do |config|\n ## == Routing ==\n config.root_path = \"/avo\"\n # used only when you have custom `map` configuration in your config.ru\n # config.prefix_path = \"/internal\"\n\n # Where should the user be redirected when visting the `/avo` url\n config.home_path = defined?(Avo::Pro) ? \"/avo/dashboards/overview\" : nil\n\n ## == Licensing ==\n # Add your license key here for Pro or Advanced licenses\n # config.license_key = ENV['AVO_LICENSE_KEY']\n\n ## == Set the context ==\n config.set_context do\n # Return a context object that gets evaluated in Avo::ApplicationController\n {}\n end\n\n ## == Authentication ==\n # config.current_user_method = {}\n # config.authenticate_with do\n # end\n\n ## == Authorization ==\n # config.authorization_methods = {\n # index: 'index?',\n # show: 'show?',\n # edit: 'edit?',\n # new: 'new?',\n # update: 'update?',\n # create: 'create?',\n # destroy: 'destroy?',\n # search: 'search?',\n # }\n # config.raise_error_on_missing_policy = false\n # config.authorization_client = :pundit\n\n ## == Localization ==\n # config.locale = 'en-US'\n\n ## == Resource options ==\n # config.resource_controls_placement = :right\n # config.model_resource_mapping = {}\n # config.default_view_type = :table\n # config.per_page = 24\n # config.per_page_steps = [12, 24, 48, 72]\n # config.via_per_page = 8\n config.id_links_to_resource = true\n # config.cache_resources_on_index_view = true\n ## permanent enable or disable cache_resource_filters, default value is false\n # config.cache_resource_filters = false\n ## provide a lambda to enable or disable cache_resource_filters per user/resource.\n # config.cache_resource_filters = ->(current_user:, resource:) { current_user.cache_resource_filters?}\n\n ## == Customization ==\n # config.app_name = 'Avocadelicious'\n # config.timezone = 'UTC'\n # config.currency = 'USD'\n # config.hide_layout_when_printing = false\n # config.full_width_container = false\n # config.full_width_index_view = false\n # config.search_debounce = 300\n # config.view_component_path = \"app/components\"\n # config.display_license_request_timeout_error = true\n # config.disabled_features = []\n # config.tabs_style = :tabs # can be :tabs or :pills\n # config.buttons_on_form_footers = true\n # config.field_wrapper_layout = true\n\n ## == Branding ==\n # config.branding = {\n # colors: {\n # background: \"248 246 242\",\n # 100 => \"#CEE7F8\",\n # 400 => \"#399EE5\",\n # 500 => \"#0886DE\",\n # 600 => \"#066BB2\",\n # },\n # chart_colors: [\"#0B8AE2\", \"#34C683\", \"#2AB1EE\", \"#34C6A8\"],\n # logo: \"/avo-assets/logo.png\",\n # logomark: \"/avo-assets/logomark.png\",\n # placeholder: \"/avo-assets/placeholder.svg\",\n # favicon: \"/avo-assets/favicon.ico\"\n # }\n\n ## == Breadcrumbs ==\n # config.display_breadcrumbs = true\n # config.set_initial_breadcrumbs do\n # add_breadcrumb \"Home\", '/avo'\n # end\n\n ## == Menus ==\n config.main_menu = -> {\n section \"Dashboards\", icon: \"dashboards\" do\n all_dashboards\n end\n\n section \"Users\", icon: \"heroicons/outline/user-group\" do\n resource :announcement\n resource :user\n resource :account\n resource :account_user\n resource :plan\n end\n\n section \"Pay\", icon: \"heroicons/outline/currency-dollar\" do\n resource :customer\n resource :charge\n resource :payment_method\n resource :subscription\n end\n\n if Rails.env.development?\n link \"Jumpstart Config\", path: Avo::Current.view_context.main_app.jumpstart_path\n end\n }\n config.profile_menu = -> {\n link \"Profile\", path: \"/avo/profile\", icon: \"user-circle\"\n }\nend\n"}
if ARGV.include? "--community-edition"
edition = "community"
elsif ARGV.include? "--pro-edition"
edition = "pro"
elsif ARGV.include? "--advanced-edition"
edition = "advanced"
end
unless edition
# === Fetch the Avo edition ===
question = <<~QUESTION
Which version of Avo would you like to install?
1. Avo Community (default)
2. Avo Pro
3. Avo Advanced
More information about version features here:
https://avohq.io/pricing
QUESTION
puts question
answer = ask("Which version of Avo would you like to install?", default: "1", limited_to: ["1", "2", "3"])
edition = case answer
when "1"
"community"
when "2"
"pro"
when "3"
"advanced"
end
end
# === Add gem to Gemfile ===
case edition
when "community"
gem "avo", ">= 3.1.0"
when "pro"
gem "avo", ">= 3.1.0"
gem "avo-pro", source: "https://packager.dev/avo-hq"
when "advanced"
gem "avo", ">= 3.1.0"
gem "avo-advanced", source: "https://packager.dev/avo-hq"
end
# === Run bundle install ===
Bundler.with_unbundled_env { run "bundle install" }
# === Add route ===
route_contents = <<-ROUTES
# Avo admin panel
if defined?(Avo::Engine)
authenticated :user, lambda { |u| u.admin? } do
mount Avo::Engine, at: Avo.configuration.root_path
end
end
ROUTES
route route_contents
# === Copy template files ===
files.each do |path, contents|
file path, contents
end