diff --git a/.cursor/rules/project-conventions.mdc b/.cursor/rules/project-conventions.mdc
index 46d54ed4233..0be4b3d5564 100644
--- a/.cursor/rules/project-conventions.mdc
+++ b/.cursor/rules/project-conventions.mdc
@@ -14,8 +14,8 @@ This rule serves as high-level documentation for how you should write code in th
- TailwindCSS for styles
- Lucide Icons for icons
- OpenAI for AI chat
-- Database: PostgreSQL
-- Jobs: Sidekiq + Redis
+- Database: Sqlite3
+- Jobs: SolidQueue
- External
- Payments: Stripe
- User bank data syncing: Plaid
diff --git a/CLAUDE.md b/CLAUDE.md
index 4fc9fcf107d..9ba9d9835d1 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -5,7 +5,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Common Development Commands
### Development Server
-- `bin/dev` - Start development server (Rails, Sidekiq, Tailwind CSS watcher)
+- `bin/dev` - Start development server (Rails, Solid queue, Tailwind CSS watcher)
- `bin/rails server` - Start Rails server only
- `bin/rails console` - Open Rails console
@@ -126,11 +126,11 @@ Provider support notes:
- Lunchflow: does not currently store pending metadata.
### Background Processing
-Sidekiq handles asynchronous tasks:
+SolidQueue handles asynchronous tasks:
- Account syncing (`SyncJob`)
- Import processing (`ImportJob`)
- AI chat responses (`AssistantResponseJob`)
-- Scheduled maintenance via sidekiq-cron
+- Scheduled maintenance via SolidQueue
### Frontend Architecture
- **Hotwire Stack**: Turbo + Stimulus for reactive UI without heavy JavaScript
diff --git a/README.md b/README.md
index 809f013cbbe..d21333a6373 100644
--- a/README.md
+++ b/README.md
@@ -71,8 +71,6 @@ The instructions below are for developers to get started with contributing to th
### Requirements
- See `.ruby-version` file for required Ruby version
-- PostgreSQL >9.3 (latest stable version recommended)
-- Redis > 5.4 (latest stable version recommended)
### Getting Started
```sh
diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb
index 8415947fc05..50dbe7bd883 100644
--- a/app/channels/application_cable/connection.rb
+++ b/app/channels/application_cable/connection.rb
@@ -4,7 +4,7 @@ class Connection < ActionCable::Connection::Base
private
def report_error(e)
- Sentry.capture_exception(e)
+ Rails.error.report(error)
end
end
end
diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb
index 6d5e6b9fce5..befa2ef8a4b 100644
--- a/app/controllers/categories_controller.rb
+++ b/app/controllers/categories_controller.rb
@@ -5,7 +5,6 @@ class CategoriesController < ApplicationController
def index
@categories = Current.family.categories.alphabetically
-
render layout: "settings"
end
@@ -15,8 +14,12 @@ def new
end
def create
- @category = Current.family.categories.new(category_params)
-
+ params = if category_params[:parent_id].blank?
+ category_params.except(:parent_id)
+ else
+ category_params
+ end
+ @category = Current.family.categories.new(params)
if @category.save
@transaction.update(category_id: @category.id) if @transaction
diff --git a/app/controllers/concerns/onboardable.rb b/app/controllers/concerns/onboardable.rb
index 60655094015..f2e69caf4c7 100644
--- a/app/controllers/concerns/onboardable.rb
+++ b/app/controllers/concerns/onboardable.rb
@@ -25,6 +25,7 @@ def redirectable_path?(path)
return false if path.starts_with?("/subscription")
return false if path.starts_with?("/onboarding")
return false if path.starts_with?("/users")
+ return false if path.starts_with?("/jobs")
return false if path.starts_with?("/api") # Exclude API endpoints from onboarding redirects
[
diff --git a/app/controllers/concerns/self_hostable.rb b/app/controllers/concerns/self_hostable.rb
index 3631571aea9..a863f17026b 100644
--- a/app/controllers/concerns/self_hostable.rb
+++ b/app/controllers/concerns/self_hostable.rb
@@ -3,8 +3,6 @@ module SelfHostable
included do
helper_method :self_hosted?, :self_hosted_first_login?
-
- prepend_before_action :verify_self_host_config
end
private
@@ -15,29 +13,4 @@ def self_hosted?
def self_hosted_first_login?
self_hosted? && User.count.zero?
end
-
- def verify_self_host_config
- return unless self_hosted?
-
- # Special handling for Redis configuration error page
- if controller_name == "pages" && action_name == "redis_configuration_error"
- # If Redis is now working, redirect to home
- if redis_connected?
- redirect_to root_path, notice: "Redis is now configured properly! You can now setup your Sure application."
- end
-
- return
- end
-
- unless redis_connected?
- redirect_to redis_configuration_error_path
- end
- end
-
- def redis_connected?
- Redis.new.ping
- true
- rescue Redis::CannotConnectError
- false
- end
end
diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb
index e0802c1c5b8..66b12fba0ca 100644
--- a/app/controllers/pages_controller.rb
+++ b/app/controllers/pages_controller.rb
@@ -1,9 +1,6 @@
class PagesController < ApplicationController
include Periodable
- skip_authentication only: %i[redis_configuration_error privacy terms]
- before_action :ensure_intro_guest!, only: :intro
-
def dashboard
if Current.user&.ui_layout_intro?
redirect_to chats_path and return
@@ -62,18 +59,6 @@ def feedback
render layout: "settings"
end
- def redis_configuration_error
- render layout: "blank"
- end
-
- def privacy
- render layout: "blank"
- end
-
- def terms
- render layout: "blank"
- end
-
private
def preferences_params
prefs = params.require(:preferences)
diff --git a/app/controllers/webhooks_controller.rb b/app/controllers/webhooks_controller.rb
index e50325dcd0e..c1e2271a3e0 100644
--- a/app/controllers/webhooks_controller.rb
+++ b/app/controllers/webhooks_controller.rb
@@ -3,57 +3,52 @@ class WebhooksController < ApplicationController
skip_authentication
def plaid
- webhook_body = request.body.read
- plaid_verification_header = request.headers["Plaid-Verification"]
+ Rails.error.handle(fallback: -> {
+ render json: { error: "Invalid webhook:" }, status: :bad_request
+ }) do
+ webhook_body = request.body.read
+ plaid_verification_header = request.headers["Plaid-Verification"]
- client = Provider::Registry.plaid_provider_for_region(:us)
+ client = Provider::Registry.plaid_provider_for_region(:us)
- client.validate_webhook!(plaid_verification_header, webhook_body)
+ client.validate_webhook!(plaid_verification_header, webhook_body)
- PlaidItem::WebhookProcessor.new(webhook_body).process
+ PlaidItem::WebhookProcessor.new(webhook_body).process
- render json: { received: true }, status: :ok
- rescue => error
- Sentry.capture_exception(error)
- Rails.logger.error("Webhook error: #{error.class} - #{error.message}")
- render json: { error: "Invalid webhook" }, status: :bad_request
+ render json: { received: true }, status: :ok
+ end
end
def plaid_eu
- webhook_body = request.body.read
- plaid_verification_header = request.headers["Plaid-Verification"]
+ Rails.error.handle(fallback: -> {
+ render json: { error: "Invalid webhook" }, status: :bad_request
+ }) do
+ webhook_body = request.body.read
+ plaid_verification_header = request.headers["Plaid-Verification"]
- client = Provider::Registry.plaid_provider_for_region(:eu)
+ client = Provider::Registry.plaid_provider_for_region(:eu)
- client.validate_webhook!(plaid_verification_header, webhook_body)
+ client.validate_webhook!(plaid_verification_header, webhook_body)
- PlaidItem::WebhookProcessor.new(webhook_body).process
+ PlaidItem::WebhookProcessor.new(webhook_body).process
- render json: { received: true }, status: :ok
- rescue => error
- Sentry.capture_exception(error)
- Rails.logger.error("Webhook error: #{error.class} - #{error.message}")
- render json: { error: "Invalid webhook" }, status: :bad_request
+ render json: { received: true }, status: :ok
+ end
end
def stripe
stripe_provider = Provider::Registry.get_provider(:stripe)
- begin
+ Rails.error.handle(JSON::ParserError, Stripe::SignatureVerificationError, fallback: -> {
+ Rails.logger.error "Stripe webhook processing error"
+ head :bad_request
+ }) do
webhook_body = request.body.read
sig_header = request.env["HTTP_STRIPE_SIGNATURE"]
stripe_provider.process_webhook_later(webhook_body, sig_header)
head :ok
- rescue JSON::ParserError => error
- Sentry.capture_exception(error)
- Rails.logger.error "JSON parser error: #{error.message}"
- head :bad_request
- rescue Stripe::SignatureVerificationError => error
- Sentry.capture_exception(error)
- Rails.logger.error "Stripe signature verification error: #{error.message}"
- head :bad_request
end
end
end
diff --git a/app/jobs/seed_dump_job.rb b/app/jobs/seed_dump_job.rb
new file mode 100644
index 00000000000..de78c8fe7c9
--- /dev/null
+++ b/app/jobs/seed_dump_job.rb
@@ -0,0 +1,57 @@
+class SeedDumpJob < ApplicationJob
+ queue_as :scheduled
+
+ def perform
+ return if Rails.env.development?
+
+ models = [
+ "Setting",
+ "ActiveStorage::VariantRecord",
+ "ActiveStorage::Attachment",
+ "ActiveStorage::Blob",
+ "Security::Price",
+ "Rule::Condition",
+ "Rule::Action",
+ "TransactionImport",
+ "Import::Row",
+ "Valuation",
+ "User",
+ "Transfer",
+ "Transaction",
+ "Trade",
+ "Tagging",
+ "Sync",
+ "Session",
+ "Security",
+ "Rule",
+ "RejectedTransfer",
+ "Investment",
+ "Holding",
+ "Family",
+ "ExchangeRate",
+ "Entry",
+ "Depository",
+ "DataEnrichment",
+ "Crypto",
+ "CreditCard",
+ "Chat",
+ "BudgetCategory",
+ "Budget",
+ "Balance",
+ "Account",
+ "Tag",
+ "Category",
+ "Import::TagMapping",
+ "Import::CategoryMapping",
+ "ToolCall::Function",
+ "UserMessage",
+ "AssistantMessage"
+ ].join(",")
+ SeedDump.dump_using_environment({
+ "IMPORT" => true,
+ "FILE" => "db/seeds/db_restore.rb",
+ "EXCLUDE" => "",
+ "MODELS" => models
+ })
+ end
+end
diff --git a/app/models/sync.rb b/app/models/sync.rb
index d1ba07a26db..b4acc4dec54 100644
--- a/app/models/sync.rb
+++ b/app/models/sync.rb
@@ -60,10 +60,10 @@ def clean
end
def perform
- Rails.logger.tagged("Sync", id, syncable_type, syncable_id) do
- # This can happen on server restarts or if Sidekiq enqueues a duplicate job
+ Rails.event.tagged("Sync", id, syncable_type, syncable_id) do
+ # This can happen on server restarts or if ~~Sidekiq~~ SolidQueue enqueues a duplicate job
unless may_start?
- Rails.logger.warn("Sync #{id} is not in a valid state (#{aasm.from_state}) to start. Skipping sync.")
+ Rails.event.notify("Sync", { id: id, syncable_type: syncable_type, syncable_id: syncable_id, message: "not in a valid state to start" })
return
end
@@ -88,6 +88,7 @@ def perform
start!
begin
+ Rails.event.notify("Performing sync", { syncable_type: syncable_type, syncable_id: syncable.id })
syncable.perform_sync(self)
rescue => e
fail!
@@ -160,27 +161,25 @@ def all_children_finalized?
end
def perform_post_sync
- Rails.logger.info("Performing post-sync for #{syncable_type} (#{syncable.id})")
+ Rails.event.notify("Performing post-sync", { syncable_type: syncable_type, syncable_id: syncable.id })
syncable.perform_post_sync
syncable.broadcast_sync_complete
rescue => e
- Rails.logger.error("Error performing post-sync for #{syncable_type} (#{syncable.id}): #{e.message}")
+ Rails.event.notify("Error performing post-sync", { syncable_type: syncable_type, syncable_id: syncable.id, error: e.message })
report_error(e)
end
def report_error(error)
- Sentry.capture_exception(error) do |scope|
- scope.set_tags(sync_id: id)
- end
+ Rails.error.report(error, context: { sync_id: id })
end
def report_warnings
todays_sync_count = syncable.syncs.where(created_at: Date.current.all_day).count
- if todays_sync_count > 10
- Sentry.capture_exception(
+ if todays_sync_count > 100
+ Rails.error.report(
Error.new("#{syncable_type} (#{syncable.id}) has exceeded 10 syncs today (count: #{todays_sync_count})"),
- level: :warning
+ context: { count: todays_sync_count, syncable_type: syncable_type, syncable_id: syncable.id }
)
end
end
diff --git a/app/views/impersonation_sessions/_super_admin_bar.html.erb b/app/views/impersonation_sessions/_super_admin_bar.html.erb
index a09a8250fa3..105e26e15fd 100644
--- a/app/views/impersonation_sessions/_super_admin_bar.html.erb
+++ b/app/views/impersonation_sessions/_super_admin_bar.html.erb
@@ -4,7 +4,7 @@
Super Admin
- <%= link_to "Jobs", sidekiq_web_url, class: "text-white underline hover:text-gray-100" %>
+ <%= link_to "Jobs", "/jobs", class: "text-white underline hover:text-gray-100" %>
diff --git a/app/views/pages/redis_configuration_error.html.erb b/app/views/pages/redis_configuration_error.html.erb
deleted file mode 100644
index a95e00862b4..00000000000
--- a/app/views/pages/redis_configuration_error.html.erb
+++ /dev/null
@@ -1,59 +0,0 @@
-<% content_for :title, "Redis Configuration Required - Sure" %>
-
-
-
-
-
-
-
- <%= icon "alert-triangle", class: "w-8 h-8 text-red-600" %>
-
-
Redis Configuration Required
-
Your self-hosted Sure installation needs Redis to be properly configured.
-
-
-
-
-
-
- <%= icon "info", class: "w-5 h-5 text-amber-600 mt-0.5 mr-3 flex-shrink-0" %>
-
-
Why is Redis required?
-
Sure uses Redis to power Sidekiq background jobs for tasks like syncing account data, processing imports, and other background operations that keep your financial data up to date.
-
-
-
-
-
-
- <%= render DS::Link.new(
- text: "View Setup Guide",
- href: "https://github.com/we-promise/sure/blob/main/docs/hosting/docker.md",
- variant: "primary",
- size: "lg",
- icon: "external-link",
- full_width: true,
- target: "_blank",
- rel: "noopener noreferrer"
- ) %>
-
Follow our complete Docker setup guide to configure Redis
-
-
-
-
-
-
-
Once you've configured Redis, refresh this page to continue.
- <%= render DS::Button.new(
- text: "Refresh Page",
- variant: "secondary",
- icon: "refresh-cw",
- type: "button",
- full_width: true,
- onclick: "window.location.reload()"
- ) %>
-
-
-
-
-
diff --git a/config/brakeman.ignore b/config/brakeman.ignore
index ca044eb6ce4..90e2d2eee67 100644
--- a/config/brakeman.ignore
+++ b/config/brakeman.ignore
@@ -161,41 +161,7 @@
95
],
"note": "Uses similar pattern to Rails internal form builder"
- },
- {
- "warning_type": "Dynamic Render Path",
- "warning_code": 15,
- "fingerprint": "fb6f7abeabc405d6882ffd41dbe8016403ef39307a5c6b4cd7b18adfaf0c24bf",
- "check_name": "Render",
- "message": "Render path contains parameter value",
- "file": "app/views/import/configurations/show.html.erb",
- "line": 34,
- "link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
- "code": "render(partial => permitted_import_configuration_path(Current.family.imports.find(params[:import_id])), { :locals => ({ :import => Current.family.imports.find(params[:import_id]) }) })",
- "render_path": [
- {
- "type": "controller",
- "class": "Import::ConfigurationsController",
- "method": "show",
- "line": 7,
- "file": "app/controllers/import/configurations_controller.rb",
- "rendered": {
- "name": "import/configurations/show",
- "file": "app/views/import/configurations/show.html.erb"
- }
- }
- ],
- "location": {
- "type": "template",
- "template": "import/configurations/show"
- },
- "user_input": "params[:import_id]",
- "confidence": "Weak",
- "cwe_id": [
- 22
- ],
- "note": ""
}
],
- "brakeman_version": "7.1.0"
+ "brakeman_version": "8.0.4"
}
diff --git a/config/honeybadger.yml b/config/honeybadger.yml
new file mode 100644
index 00000000000..685e2bb7ec8
--- /dev/null
+++ b/config/honeybadger.yml
@@ -0,0 +1,30 @@
+---
+# For more options, see https://docs.honeybadger.io/lib/ruby/gem-reference/configuration
+
+api_key: "<%= ENV["HONEYBADGER_API_KEY"] %>"
+
+# The environment your app is running in.
+env: "<%= Rails.env %>"
+
+# The absolute path to your project folder.
+root: "<%= Rails.root.to_s %>"
+
+# Honeybadger won't report errors in these environments.
+development_environments:
+- test
+- development
+- cucumber
+
+# By default, Honeybadger won't report errors in the development_environments.
+# You can override this by explicitly setting report_data to true.
+# report_data: true
+
+# The current Git revision of your project. Defaults to the last commit hash.
+# revision: null
+
+# Enable verbose debug logging (useful for troubleshooting).
+debug: false
+
+# Enable Honeybadger Insights
+insights:
+ enabled: true
diff --git a/config/initializers/litestream.rb b/config/initializers/litestream.rb
new file mode 100644
index 00000000000..0705b5b68e6
--- /dev/null
+++ b/config/initializers/litestream.rb
@@ -0,0 +1,45 @@
+# Use this hook to configure the litestream-ruby gem.
+# All configuration options will be available as environment variables, e.g.
+# config.replica_bucket becomes LITESTREAM_REPLICA_BUCKET
+# This allows you to configure Litestream using Rails encrypted credentials,
+# or some other mechanism where the values are only available at runtime.
+
+Rails.application.configure do
+ # Configure Litestream through environment variables. Use Rails encrypted credentials for secrets.
+ # litestream_credentials = Rails.application.credentials.litestream
+
+ # Replica-specific bucket location. This will be your bucket's URL without the `https://` prefix.
+ # For example, if you used DigitalOcean Spaces, your bucket URL could look like:
+ #
+ # https://myapp.fra1.digitaloceanspaces.com
+ #
+ # And so you should set your `replica_bucket` to:
+ #
+ # myapp.fra1.digitaloceanspaces.com
+ #
+ # config.litestream.replica_bucket = litestream_credentials&.replica_bucket
+ #
+ # Replica-specific authentication key. Litestream needs authentication credentials to access your storage provider bucket.
+ # config.litestream.replica_key_id = litestream_credentials&.replica_key_id
+ #
+ # Replica-specific secret key. Litestream needs authentication credentials to access your storage provider bucket.
+ # config.litestream.replica_access_key = litestream_credentials&.replica_access_key
+ #
+ # Replica-specific region. Set the bucket’s region. Only used for AWS S3 & Backblaze B2.
+ # config.litestream.replica_region = "us-east-1"
+ #
+ # Replica-specific endpoint. Set the endpoint URL of the S3-compatible service. Only required for non-AWS services.
+ # config.litestream.replica_endpoint = "endpoint.your-objectstorage.com"
+
+ # Configure the default Litestream config path
+ # config.config_path = Rails.root.join("config", "litestream.yml")
+
+ # Configure the Litestream dashboard
+ #
+ # Set the default base controller class
+ config.litestream.base_controller_class = "ActionController::Base"
+ #
+ # Set authentication credentials for Litestream dashboard
+ # config.litestream.username = litestream_credentials&.username
+ # config.litestream.password = litestream_credentials&.password
+end
diff --git a/config/initializers/rails_db.rb b/config/initializers/rails_db.rb
new file mode 100644
index 00000000000..06ffbf2416d
--- /dev/null
+++ b/config/initializers/rails_db.rb
@@ -0,0 +1,30 @@
+if Object.const_defined?("RailsDb")
+ RailsDb.setup do |config|
+ # # enabled or not
+ config.enabled = true
+
+ # # automatic engine routes mounting
+ config.automatic_routes_mount = true
+
+ # set tables which you want to hide ONLY
+ # config.black_list_tables = [ "users", "accounts" ]
+
+ # set tables which you want to show ONLY
+ # config.white_list_tables = [ "posts", "comments" ]
+
+ # # Enable http basic authentication
+ config.http_basic_authentication_enabled = true if Rails.env.production?
+
+ # # Enable http basic authentication
+ config.http_basic_authentication_user_name = "rails_db" if Rails.env.production?
+
+ # # Enable http basic authentication
+ config.http_basic_authentication_password = ENV.fetch("RAILS_DB_PASSWORD", "rails_db") if Rails.env.production?
+
+ # # Enable verify access proc
+ # config.verify_access_proc = proc { |controller| true }
+
+ # # Sandbox mode (only read-only operations)
+ # config.sandbox = false
+ end
+end
diff --git a/config/initializers/rswag_ui.rb b/config/initializers/rswag_ui.rb
new file mode 100644
index 00000000000..417b5a63294
--- /dev/null
+++ b/config/initializers/rswag_ui.rb
@@ -0,0 +1,3 @@
+Rswag::Ui.configure do |c|
+ c.swagger_endpoint "/api-docs.yaml", "Maybe Finance API V1"
+end
diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb
index 8e31567d49c..84363148ed0 100644
--- a/config/initializers/sentry.rb
+++ b/config/initializers/sentry.rb
@@ -22,5 +22,7 @@
config.release = Rails.root.join(".sure-version").read.strip rescue nil
config.profiler_class = Sentry::Vernier::Profiler
+ config.enable_logs = true
+ config.enabled_patches = [ :logger, :http, :puma ]
end
end
diff --git a/config/initializers/subscribers.rb b/config/initializers/subscribers.rb
new file mode 100644
index 00000000000..9ab6ca4217b
--- /dev/null
+++ b/config/initializers/subscribers.rb
@@ -0,0 +1,36 @@
+# Event Subscriber
+class LogEventSubscriber
+ def emit(event)
+ payload = event[:payload].map { |key, value| "#{key}=#{value}" }.join(" ")
+
+ source_location = event[:source_location]
+
+ log = "[#{event[:name]}] #{payload} at #{source_location[:filepath]}:#{source_location[:lineno]}"
+ Rails.logger.info(log)
+ Honeybadger.event(event[:name], payload: event[:payload], source_location: source_location)
+ end
+end
+
+# Error Subscriber
+class LogErrorSubscriber
+ def report(error, handled:, severity:, context:, source:)
+ error_message = "#{error.class}: #{error.message}"
+ error_message += " (handled: #{handled}, severity: #{severity}, source: #{source})"
+
+ if context.any?
+ context_str = context.map { |k, v| "#{k}=#{v}" }.join(", ")
+ error_message += " [context: #{context_str}]"
+ end
+
+ Rails.logger.error("[Error] #{error_message}")
+
+ # Log backtrace in development
+ if Rails.env.development? && error.backtrace
+ Rails.logger.error("Backtrace:\n#{error.backtrace.first(10).join("\n")}")
+ end
+ end
+end
+
+# Register subscribers
+Rails.event.subscribe(LogEventSubscriber.new)
+Rails.error.subscribe(LogErrorSubscriber.new)
diff --git a/config/litestream.yml b/config/litestream.yml
new file mode 100644
index 00000000000..aa3ee19e4cd
--- /dev/null
+++ b/config/litestream.yml
@@ -0,0 +1,21 @@
+# This is the actual configuration file for litestream.
+#
+# You can either use the generated `config/initializers/litestream.rb`
+# file to configure the litestream-ruby gem, which will populate these
+# ENV variables when using the `rails litestream:replicate` command.
+#
+# Or, if you prefer, manually manage ENV variables and this configuration file.
+# In that case, simply ensure that the ENV variables are set before running the
+# `replicate` command.
+#
+# For more details, see: https://litestream.io/reference/config/
+dbs:
+ - path: storage/production_primary.sqlite3
+ replicas:
+ - type: s3
+ bucket: sure-backups
+ endpoint: $LITESTREAM_REPLICA_BUCKET_URL
+ path: storage/production_primary.sqlite3
+ access-key-id: $LITESTREAM_ACCESS_KEY_ID
+ secret-access-key: $LITESTREAM_SECRET_ACCESS_KEY
+ validation-interval: 24h
diff --git a/config/routes.rb b/config/routes.rb
index 48116d5c492..38208a838ee 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,118 +1,6 @@
-require "sidekiq/web"
-require "sidekiq/cron/web"
-
Rails.application.routes.draw do
- resources :indexa_capital_items, only: [ :index, :new, :create, :show, :edit, :update, :destroy ] do
- collection do
- get :preload_accounts
- get :select_accounts
- post :link_accounts
- get :select_existing_account
- post :link_existing_account
- end
-
- member do
- post :sync
- get :setup_accounts
- post :complete_account_setup
- end
- end
- resources :mercury_items, only: %i[index new create show edit update destroy] do
- collection do
- get :preload_accounts
- get :select_accounts
- post :link_accounts
- get :select_existing_account
- post :link_existing_account
- end
-
- member do
- post :sync
- get :setup_accounts
- post :complete_account_setup
- end
- end
-
- resources :coinbase_items, only: [ :index, :new, :create, :show, :edit, :update, :destroy ] do
- collection do
- get :preload_accounts
- get :select_accounts
- post :link_accounts
- get :select_existing_account
- post :link_existing_account
- end
-
- member do
- post :sync
- get :setup_accounts
- post :complete_account_setup
- end
- end
+ mount Rswag::Ui::Engine => "/api-docs"
- resources :binance_items, only: [ :index, :new, :create, :show, :edit, :update, :destroy ] do
- collection do
- get :select_accounts
- post :link_accounts
- get :select_existing_account
- post :link_existing_account
- end
-
- member do
- post :sync
- get :setup_accounts
- post :complete_account_setup
- end
- end
-
- resources :snaptrade_items, only: [ :index, :new, :create, :show, :edit, :update, :destroy ] do
- collection do
- get :preload_accounts
- get :select_accounts
- post :link_accounts
- get :select_existing_account
- post :link_existing_account
- get :callback
- end
-
- member do
- post :sync
- get :connect
- get :setup_accounts
- post :complete_account_setup
- get :connections
- delete :delete_connection
- delete :delete_orphaned_user
- end
- end
-
- # CoinStats routes
- resources :coinstats_items, only: [ :index, :new, :create, :update, :destroy ] do
- collection do
- post :link_wallet
- post :link_exchange
- end
- member do
- post :sync
- end
- end
-
- resources :enable_banking_items, only: [ :new, :create, :update, :destroy ] do
- collection do
- get :callback
- post :link_accounts
- get :select_existing_account
- post :link_existing_account
- end
- member do
- post :sync
- get :select_bank
- post :authorize
- post :reauthorize
- get :setup_accounts
- post :complete_account_setup
- post :new_connection
- end
- end
use_doorkeeper
# MFA routes
resource :mfa, controller: "mfa", only: [ :new, :create ] do
@@ -125,13 +13,8 @@
mount Lookbook::Engine, at: "/design-system" if Rails.env.development?
- if Rails.env.development?
- mount Rswag::Api::Engine => "/api-docs"
- mount Rswag::Ui::Engine => "/api-docs"
- end
-
- # Uses basic auth - see config/initializers/sidekiq.rb
- mount Sidekiq::Web => "/sidekiq"
+ mount MissionControl::Jobs::Engine, at: "/jobs"
+ mount Litestream::Engine, at: "/litestream" if Rails.env.production?
# AI chats
resources :chats do
@@ -550,11 +433,6 @@
post "stripe"
end
- get "redis-configuration-error", to: "pages#redis_configuration_error"
-
- # MCP server endpoint for external AI assistants (JSON-RPC 2.0)
- post "mcp", to: "mcp#handle"
-
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
# Can be used by load balancers and uptime monitors to verify that the app is live.
get "up" => "rails/health#show", as: :rails_health_check
diff --git a/docs/hosting/docker.md b/docs/hosting/docker.md
index 5ace300ab3d..8eb0fa6fe25 100644
--- a/docs/hosting/docker.md
+++ b/docs/hosting/docker.md
@@ -92,7 +92,6 @@ Fill in this file with the following variables:
```txt
SECRET_KEY_BASE="replacemewiththegeneratedstringfromthepriorstep"
-POSTGRES_PASSWORD="replacemewithyourdesireddatabasepassword"
```
We also recommend leaving YJIT disabled by default on smaller hosts. If you want to trade more memory for potentially better Ruby throughput, opt in explicitly:
@@ -298,26 +297,3 @@ docker compose pull # This pulls the "latest" published image from GHCR
docker compose build # This rebuilds the app with updates
docker compose up --no-deps -d web worker # This restarts the app using the newest version
```
-
-## Troubleshooting
-
-### ActiveRecord::DatabaseConnectionError
-
-If you are trying to get Sure started for the **first time** and run into database connection issues, it is likely because Docker has already initialized the Postgres database with a _different_ default role (usually from a previous attempt to start the app).
-
-If you run into this issue, you can optionally **reset the database**.
-
-**PLEASE NOTE: this will delete any existing data that you have in your Sure database, so proceed with caution.** For first-time users of the app just trying to get started, you're generally safe to run the commands below.
-
-By running the commands below, you will delete your existing Sure database and "reset" it.
-
-```
-docker compose down
-docker volume rm sure_postgres-data # this is the name of the volume the DB is mounted to
-docker compose up
-docker compose exec db psql -U sure_user -d sure_development -c "SELECT 1;" # This will verify that the issue is fixed
-```
-
-### Slow `.csv` import (processing rows taking longer than expected)
-
-Importing comma-separated-value file(s) requires the `sure-worker` container to communicate with Redis. Check your worker logs for any unexpected errors, such as connection timeouts or Redis communication failures.
diff --git a/public/api-docs.yaml b/public/api-docs.yaml
new file mode 100644
index 00000000000..45392f7212e
--- /dev/null
+++ b/public/api-docs.yaml
@@ -0,0 +1,1079 @@
+openapi: 3.0.3
+info:
+ title: Maybe Finance API
+ description: |
+ The Maybe Finance API allows external applications to interact with your financial data.
+
+ ## Authentication
+
+ The API supports two authentication methods:
+
+ 1. **OAuth 2.0**: Pass the access token in the `Authorization` header as `Bearer
`
+ 2. **API Key**: Pass the API key in the `X-Api-Key` header
+
+ ## Scopes
+
+ - `read`: Read access to resources
+ - `read_write`: Full read and write access
+
+ ## Rate Limiting
+
+ API requests are subject to rate limiting based on your authentication method and tier.
+ version: 1.0.0
+ contact:
+ name: Maybe Finance Support
+ url: https://maybe.co/support
+
+servers:
+ - url: https://api.maybe.co
+ description: Production server
+ - url: http://localhost:3000
+ description: Local development
+
+security:
+ - bearerAuth: []
+ - apiKeyAuth: []
+
+paths:
+ /api/v1/auth/signup:
+ post:
+ summary: User signup
+ description: Create a new user account with OAuth application
+ tags:
+ - Authentication
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - user
+ - device
+ properties:
+ user:
+ type: object
+ required:
+ - email
+ - password
+ - first_name
+ - last_name
+ properties:
+ email:
+ type: string
+ format: email
+ password:
+ type: string
+ minLength: 8
+ first_name:
+ type: string
+ last_name:
+ type: string
+ device:
+ type: object
+ required:
+ - device_id
+ - device_name
+ - device_type
+ - os_version
+ - app_version
+ properties:
+ device_id:
+ type: string
+ device_name:
+ type: string
+ device_type:
+ type: string
+ enum: [ios, android, web]
+ os_version:
+ type: string
+ app_version:
+ type: string
+ invite_code:
+ type: string
+ description: Required if invite codes are enabled
+ responses:
+ '201':
+ description: User created successfully
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ access_token:
+ type: string
+ refresh_token:
+ type: string
+ token_type:
+ type: string
+ example: Bearer
+ expires_in:
+ type: integer
+ created_at:
+ type: integer
+ user:
+ type: object
+ properties:
+ id:
+ type: string
+ format: uuid
+ email:
+ type: string
+ first_name:
+ type: string
+ last_name:
+ type: string
+ '403':
+ description: Invite code required or invalid
+ '422':
+ description: Validation failed
+
+ /api/v1/auth/login:
+ post:
+ summary: User login
+ description: Authenticate and receive OAuth tokens
+ tags:
+ - Authentication
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - email
+ - password
+ - device
+ properties:
+ email:
+ type: string
+ format: email
+ password:
+ type: string
+ otp_code:
+ type: string
+ description: Required if MFA is enabled
+ device:
+ type: object
+ required:
+ - device_id
+ - device_name
+ - device_type
+ - os_version
+ - app_version
+ properties:
+ device_id:
+ type: string
+ device_name:
+ type: string
+ device_type:
+ type: string
+ os_version:
+ type: string
+ app_version:
+ type: string
+ responses:
+ '200':
+ description: Login successful
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ access_token:
+ type: string
+ refresh_token:
+ type: string
+ token_type:
+ type: string
+ expires_in:
+ type: integer
+ user:
+ type: object
+ properties:
+ id:
+ type: string
+ email:
+ type: string
+ first_name:
+ type: string
+ last_name:
+ type: string
+ '401':
+ description: Invalid credentials or MFA required
+
+ /api/v1/auth/refresh:
+ post:
+ summary: Refresh access token
+ description: Get a new access token using a refresh token
+ tags:
+ - Authentication
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - refresh_token
+ properties:
+ refresh_token:
+ type: string
+ device:
+ type: object
+ properties:
+ device_id:
+ type: string
+ responses:
+ '200':
+ description: Token refreshed successfully
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ access_token:
+ type: string
+ refresh_token:
+ type: string
+ token_type:
+ type: string
+ expires_in:
+ type: integer
+ created_at:
+ type: integer
+ '400':
+ description: Refresh token required
+ '401':
+ description: Invalid refresh token
+
+ /api/v1/accounts:
+ get:
+ summary: List accounts
+ description: Get all accounts belonging to the authenticated user's family
+ tags:
+ - Accounts
+ security:
+ - bearerAuth: []
+ - apiKeyAuth: []
+ parameters:
+ - name: page
+ in: query
+ schema:
+ type: integer
+ default: 1
+ - name: per_page
+ in: query
+ schema:
+ type: integer
+ default: 25
+ maximum: 100
+ responses:
+ '200':
+ description: List of accounts
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ accounts:
+ type: array
+ items:
+ $ref: '#/components/schemas/Account'
+ pagination:
+ $ref: '#/components/schemas/Pagination'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+
+ /api/v1/transactions:
+ get:
+ summary: List transactions
+ description: Get all transactions with optional filtering
+ tags:
+ - Transactions
+ security:
+ - bearerAuth: []
+ - apiKeyAuth: []
+ parameters:
+ - name: page
+ in: query
+ schema:
+ type: integer
+ default: 1
+ - name: per_page
+ in: query
+ schema:
+ type: integer
+ default: 25
+ maximum: 100
+ - name: account_id
+ in: query
+ schema:
+ type: string
+ format: uuid
+ - name: account_ids
+ in: query
+ schema:
+ type: array
+ items:
+ type: string
+ - name: category_id
+ in: query
+ schema:
+ type: string
+ format: uuid
+ - name: category_ids
+ in: query
+ schema:
+ type: array
+ items:
+ type: string
+ - name: merchant_id
+ in: query
+ schema:
+ type: string
+ format: uuid
+ - name: merchant_ids
+ in: query
+ schema:
+ type: array
+ items:
+ type: string
+ - name: start_date
+ in: query
+ schema:
+ type: string
+ format: date
+ - name: end_date
+ in: query
+ schema:
+ type: string
+ format: date
+ - name: min_amount
+ in: query
+ schema:
+ type: number
+ - name: max_amount
+ in: query
+ schema:
+ type: number
+ - name: type
+ in: query
+ schema:
+ type: string
+ enum: [income, expense]
+ - name: search
+ in: query
+ schema:
+ type: string
+ responses:
+ '200':
+ description: List of transactions
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ transactions:
+ type: array
+ items:
+ $ref: '#/components/schemas/Transaction'
+ pagination:
+ $ref: '#/components/schemas/Pagination'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+
+ post:
+ summary: Create transaction
+ description: Create a new transaction
+ tags:
+ - Transactions
+ security:
+ - bearerAuth: []
+ - apiKeyAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - transaction
+ properties:
+ transaction:
+ type: object
+ required:
+ - account_id
+ - date
+ - amount
+ - name
+ properties:
+ account_id:
+ type: string
+ format: uuid
+ date:
+ type: string
+ format: date
+ amount:
+ type: number
+ name:
+ type: string
+ description:
+ type: string
+ notes:
+ type: string
+ currency:
+ type: string
+ nature:
+ type: string
+ enum: [income, inflow, expense, outflow]
+ category_id:
+ type: string
+ format: uuid
+ merchant_id:
+ type: string
+ format: uuid
+ tag_ids:
+ type: array
+ items:
+ type: string
+ responses:
+ '201':
+ description: Transaction created
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Transaction'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '422':
+ $ref: '#/components/responses/UnprocessableEntity'
+
+ /api/v1/transactions/{id}:
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+
+ get:
+ summary: Get transaction
+ description: Get a single transaction by ID
+ tags:
+ - Transactions
+ security:
+ - bearerAuth: []
+ - apiKeyAuth: []
+ responses:
+ '200':
+ description: Transaction details
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Transaction'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '404':
+ $ref: '#/components/responses/NotFound'
+
+ patch:
+ summary: Update transaction
+ description: Update an existing transaction
+ tags:
+ - Transactions
+ security:
+ - bearerAuth: []
+ - apiKeyAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ transaction:
+ type: object
+ properties:
+ date:
+ type: string
+ format: date
+ amount:
+ type: number
+ name:
+ type: string
+ description:
+ type: string
+ notes:
+ type: string
+ nature:
+ type: string
+ enum: [income, inflow, expense, outflow]
+ category_id:
+ type: string
+ format: uuid
+ merchant_id:
+ type: string
+ format: uuid
+ tag_ids:
+ type: array
+ items:
+ type: string
+ responses:
+ '200':
+ description: Transaction updated
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Transaction'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '404':
+ $ref: '#/components/responses/NotFound'
+ '422':
+ $ref: '#/components/responses/UnprocessableEntity'
+
+ delete:
+ summary: Delete transaction
+ description: Delete a transaction
+ tags:
+ - Transactions
+ security:
+ - bearerAuth: []
+ - apiKeyAuth: []
+ responses:
+ '200':
+ description: Transaction deleted
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '404':
+ $ref: '#/components/responses/NotFound'
+
+ /api/v1/usage:
+ get:
+ summary: Get API usage
+ description: Get usage information for the current API key or OAuth token
+ tags:
+ - Usage
+ security:
+ - bearerAuth: []
+ - apiKeyAuth: []
+ responses:
+ '200':
+ description: Usage information
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ api_key:
+ type: object
+ properties:
+ name:
+ type: string
+ scopes:
+ type: array
+ items:
+ type: string
+ last_used_at:
+ type: string
+ format: date-time
+ created_at:
+ type: string
+ format: date-time
+ authentication_method:
+ type: string
+ message:
+ type: string
+ '400':
+ description: Invalid authentication method
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+
+ /api/v1/chats:
+ get:
+ summary: List chats
+ description: Get all chats for the authenticated user
+ tags:
+ - Chats
+ security:
+ - bearerAuth: []
+ - apiKeyAuth: []
+ parameters:
+ - name: page
+ in: query
+ schema:
+ type: integer
+ default: 1
+ - name: per_page
+ in: query
+ schema:
+ type: integer
+ default: 20
+ responses:
+ '200':
+ description: List of chats
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ chats:
+ type: array
+ items:
+ $ref: '#/components/schemas/Chat'
+ pagination:
+ $ref: '#/components/schemas/Pagination'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ description: AI features not enabled
+
+ post:
+ summary: Create chat
+ description: Create a new chat with an optional initial message
+ tags:
+ - Chats
+ security:
+ - bearerAuth: []
+ - apiKeyAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ title:
+ type: string
+ message:
+ type: string
+ description: Initial message to send to AI
+ model:
+ type: string
+ enum: [gpt-4, gpt-4-turbo, gpt-3.5-turbo]
+ default: gpt-4
+ responses:
+ '201':
+ description: Chat created
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ChatWithMessages'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ description: AI features not enabled
+ '422':
+ $ref: '#/components/responses/UnprocessableEntity'
+
+ /api/v1/chats/{id}:
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+
+ get:
+ summary: Get chat
+ description: Get a single chat with its messages
+ tags:
+ - Chats
+ security:
+ - bearerAuth: []
+ - apiKeyAuth: []
+ responses:
+ '200':
+ description: Chat details with messages
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ChatWithMessages'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ description: AI features not enabled
+ '404':
+ $ref: '#/components/responses/NotFound'
+
+ patch:
+ summary: Update chat
+ description: Update chat title
+ tags:
+ - Chats
+ security:
+ - bearerAuth: []
+ - apiKeyAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ title:
+ type: string
+ responses:
+ '200':
+ description: Chat updated
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ChatWithMessages'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '404':
+ $ref: '#/components/responses/NotFound'
+
+ delete:
+ summary: Delete chat
+ description: Delete a chat and all its messages
+ tags:
+ - Chats
+ security:
+ - bearerAuth: []
+ - apiKeyAuth: []
+ responses:
+ '204':
+ description: Chat deleted
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '404':
+ $ref: '#/components/responses/NotFound'
+
+ /api/v1/chats/{chat_id}/messages:
+ parameters:
+ - name: chat_id
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+
+ post:
+ summary: Create message
+ description: Send a message to the AI assistant
+ tags:
+ - Messages
+ security:
+ - bearerAuth: []
+ - apiKeyAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - content
+ properties:
+ content:
+ type: string
+ model:
+ type: string
+ enum: [gpt-4, gpt-4-turbo, gpt-3.5-turbo]
+ default: gpt-4
+ responses:
+ '201':
+ description: Message created
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Message'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ description: AI features not enabled
+ '404':
+ $ref: '#/components/responses/NotFound'
+ '422':
+ $ref: '#/components/responses/UnprocessableEntity'
+
+ /api/v1/chats/{chat_id}/messages/retry:
+ parameters:
+ - name: chat_id
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+
+ post:
+ summary: Retry last message
+ description: Retry generating the last AI assistant response
+ tags:
+ - Messages
+ security:
+ - bearerAuth: []
+ - apiKeyAuth: []
+ responses:
+ '202':
+ description: Retry initiated
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ message_id:
+ type: string
+ format: uuid
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ description: AI features not enabled
+ '404':
+ $ref: '#/components/responses/NotFound'
+ '422':
+ description: No assistant message to retry
+
+components:
+ securitySchemes:
+ bearerAuth:
+ type: http
+ scheme: bearer
+ bearerFormat: JWT
+ description: OAuth 2.0 access token
+ apiKeyAuth:
+ type: apiKey
+ in: header
+ name: X-Api-Key
+ description: API key for authentication
+
+ schemas:
+ Account:
+ type: object
+ properties:
+ id:
+ type: string
+ format: uuid
+ name:
+ type: string
+ balance:
+ type: string
+ currency:
+ type: string
+ classification:
+ type: string
+ enum: [asset, liability]
+ account_type:
+ type: string
+
+ Transaction:
+ type: object
+ properties:
+ id:
+ type: string
+ format: uuid
+ name:
+ type: string
+ amount:
+ type: string
+ currency:
+ type: string
+ date:
+ type: string
+ format: date
+ notes:
+ type: string
+ category:
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ merchant:
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ account:
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ tags:
+ type: array
+ items:
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+
+ Chat:
+ type: object
+ properties:
+ id:
+ type: string
+ format: uuid
+ title:
+ type: string
+ last_message_at:
+ type: string
+ format: date-time
+ message_count:
+ type: integer
+ error:
+ type: string
+ nullable: true
+ created_at:
+ type: string
+ format: date-time
+ updated_at:
+ type: string
+ format: date-time
+
+ ChatWithMessages:
+ allOf:
+ - $ref: '#/components/schemas/Chat'
+ - type: object
+ properties:
+ messages:
+ type: array
+ items:
+ $ref: '#/components/schemas/Message'
+
+ Message:
+ type: object
+ properties:
+ id:
+ type: string
+ format: uuid
+ chat_id:
+ type: string
+ format: uuid
+ type:
+ type: string
+ enum: [user_message, assistant_message]
+ role:
+ type: string
+ enum: [user, assistant]
+ content:
+ type: string
+ model:
+ type: string
+ created_at:
+ type: string
+ format: date-time
+ updated_at:
+ type: string
+ format: date-time
+ ai_response_status:
+ type: string
+ description: Only present for user messages
+ ai_response_message:
+ type: string
+ description: Only present for user messages
+ tool_calls:
+ type: array
+ description: Only present for assistant messages
+ items:
+ type: object
+ properties:
+ id:
+ type: string
+ function_name:
+ type: string
+ function_arguments:
+ type: object
+ function_result:
+ type: object
+ created_at:
+ type: string
+ format: date-time
+
+ Pagination:
+ type: object
+ properties:
+ page:
+ type: integer
+ per_page:
+ type: integer
+ total_count:
+ type: integer
+ total_pages:
+ type: integer
+
+ Error:
+ type: object
+ properties:
+ error:
+ type: string
+ message:
+ type: string
+ details:
+ type: array
+ items:
+ type: string
+
+ responses:
+ Unauthorized:
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ example:
+ error: unauthorized
+ message: Access token or API key is invalid, expired, or missing
+
+ Forbidden:
+ description: Forbidden - Insufficient scope or permissions
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ example:
+ error: insufficient_scope
+ message: This action requires the 'write' scope
+
+ NotFound:
+ description: Resource not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ example:
+ error: not_found
+ message: Resource not found
+
+ UnprocessableEntity:
+ description: Validation failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ example:
+ error: validation_failed
+ message: Validation failed
+ details: ["Name can't be blank"]