- ${this._getTrendIcon(datum)}
+
+
+
+
+ ${this._getTrendIcon(datum)}
+
+
${this._primarySeriesLabel}
- ${this._extractFormattedValue(datum.trend.current)}
+
+ ${this._extractFormattedValue(datum.trend.current)}
+
-
${
datum.trend.value === 0
- ? `
`
+ ? ""
: `
-
+
${this._extractFormattedValue(datum.trend.value)} (${datum.trend.percent_formatted})
-
+
`
}
+
+
+
+
${this._secondarySeriesLabel}
+
+
${this._extractFormattedValue(secondaryDatum.value)}
+
`;
}
@@ -431,6 +503,12 @@ export default class extends Controller {
return this._extractNumericValue(datum.value);
};
+ _findSecondaryDatum(date) {
+ return this._secondaryDataPoints.find(
+ (datum) => datum.date?.getTime() === date?.getTime(),
+ );
+ }
+
_extractNumericValue = (numeric) => {
if (typeof numeric === "object" && "amount" in numeric) {
return Number(numeric.amount);
@@ -505,6 +583,18 @@ export default class extends Controller {
return this.dataValue.trend.color;
}
+ get _primarySeriesLabel() {
+ return this.hasPrimarySeriesLabelValue
+ ? this.primarySeriesLabelValue
+ : "Primary";
+ }
+
+ get _secondarySeriesLabel() {
+ return this.hasSecondarySeriesLabelValue
+ ? this.secondarySeriesLabelValue
+ : "Secondary";
+ }
+
get _d3Line() {
return d3
.line()
@@ -512,16 +602,27 @@ export default class extends Controller {
.y((d) => this._d3YScale(this._getDatumValue(d)));
}
+ get _d3SecondaryLine() {
+ return d3
+ .line()
+ .x((d) => this._d3XScale(d.date))
+ .y((d) => this._d3YScale(this._getDatumValue(d)));
+ }
+
+ get _allDataPoints() {
+ return [...this._normalDataPoints, ...this._secondaryDataPoints];
+ }
+
get _d3XScale() {
return d3
.scaleTime()
.rangeRound([0, this._d3ContainerWidth])
- .domain(d3.extent(this._normalDataPoints, (d) => d.date));
+ .domain(d3.extent(this._allDataPoints, (d) => d.date));
}
get _d3YScale() {
- const dataMin = d3.min(this._normalDataPoints, this._getDatumValue);
- const dataMax = d3.max(this._normalDataPoints, this._getDatumValue);
+ const dataMin = d3.min(this._allDataPoints, this._getDatumValue);
+ const dataMax = d3.max(this._allDataPoints, this._getDatumValue);
// Handle edge case where all values are the same
if (dataMin === dataMax) {
diff --git a/app/models/balance.rb b/app/models/balance.rb
index 3b6f74ce21c..73db0dbb04d 100644
--- a/app/models/balance.rb
+++ b/app/models/balance.rb
@@ -6,6 +6,9 @@ class Balance < ApplicationRecord
validates :account, :date, :balance, presence: true
validates :flows_factor, inclusion: { in: [ -1, 1 ] }
+ before_save :calculate_derived_balances
+ after_initialize :calculate_derived_balances
+
monetize :balance, :cash_balance,
:start_cash_balance, :start_non_cash_balance, :start_balance,
:cash_inflows, :cash_outflows, :non_cash_inflows, :non_cash_outflows, :net_market_flows,
@@ -28,4 +31,28 @@ def balance_trend
def favorable_direction
flows_factor == -1 ? "down" : "up"
end
+
+ def calculate_derived_balances
+ raise ArgumentError, "flows_factor is nil" if flows_factor.nil?
+ raise ArgumentError, "start_cash_balance is nil" if start_cash_balance.nil?
+ raise ArgumentError, "start_non_cash_balance is nil" if start_non_cash_balance.nil?
+ raise ArgumentError, "cash_inflows is nil" if cash_inflows.nil?
+ raise ArgumentError, "cash_outflows is nil" if cash_outflows.nil?
+ raise ArgumentError, "non_cash_inflows is nil" if non_cash_inflows.nil?
+ raise ArgumentError, "non_cash_outflows is nil" if non_cash_outflows.nil?
+ raise ArgumentError, "net_market_flows is nil" if net_market_flows.nil?
+ raise ArgumentError, "cash_adjustments is nil" if cash_adjustments.nil?
+ raise ArgumentError, "non_cash_adjustments is nil" if non_cash_adjustments.nil?
+ # Calculate start_balance
+ self.start_balance = start_cash_balance + start_non_cash_balance
+
+ # Calculate end_cash_balance
+ self.end_cash_balance = (start_cash_balance + ((cash_inflows - cash_outflows) * flows_factor)) + cash_adjustments
+
+ # Calculate end_non_cash_balance
+ self.end_non_cash_balance = ((start_non_cash_balance + ((non_cash_inflows - non_cash_outflows) * flows_factor)) + net_market_flows) + non_cash_adjustments
+
+ # Calculate end_balance
+ self.end_balance = end_cash_balance + end_non_cash_balance
+ end
end
diff --git a/app/models/balance/chart_series_builder.rb b/app/models/balance/chart_series_builder.rb
index c8c733579e4..9fe20bbd02d 100644
--- a/app/models/balance/chart_series_builder.rb
+++ b/app/models/balance/chart_series_builder.rb
@@ -65,23 +65,43 @@ def build_series_for(column)
)
end
- def query_data
- @query_data ||= Balance.find_by_sql([
- query,
- {
- account_ids: account_ids,
- target_currency: currency,
- start_date: period.start_date,
- end_date: period.end_date,
- interval: interval,
- sign_multiplier: sign_multiplier
- }
- ])
- rescue => e
- Rails.logger.error "Query data error: #{e.message} for accounts #{account_ids}, period #{period.start_date} to #{period.end_date}"
- raise
+ def accounts
+ @accounts ||= Account.where(id: account_ids).select(:id, :currency, :name)
end
+ def exchange_rates
+ @exchange_rates ||= begin
+ ExchangeRate.where(date: (period.start_date - 30.days)..period.end_date) # extend the range so exchange rates are likely to be available
+ .and(ExchangeRate.where(to_currency: currency))
+ .and(ExchangeRate.where(from_currency: accounts.pluck(:currency).uniq))
+ .select(:id, :date, :rate, :from_currency, :to_currency)
+ .group_by { |er| [ er.from_currency, er.to_currency ] }
+ .transform_values { |rates| rates.sort_by(&:date).reverse }
+ end
+ end
+
+ def balances
+ @balances ||= Balance.where(account_id: account_ids)
+ .where(date: period.date_range)
+ .select("*")
+ .group_by { |b| [ b.account_id, b.date ] }
+ .transform_values { |balances| balances.sort_by(&:date).last }
+ end
+
+ def starting_balances
+ @starting_balances ||= begin
+ latest_dates = Balance.where("date <= ?", period.start_date)
+ .where(account_id: account_ids)
+ .group(:account_id)
+ .maximum(:date)
+
+ Balance.where(account_id: latest_dates.keys)
+ .where(date: latest_dates.values)
+ .select(:account_id, :date, :end_balance, :end_cash_balance, :end_non_cash_balance, :start_balance, :start_cash_balance, :start_non_cash_balance, :flows_factor, :cash_inflows, :cash_outflows, :non_cash_inflows, :non_cash_outflows, :net_market_flows, :cash_adjustments, :non_cash_adjustments)
+ .group_by { |b| b.account_id }
+ .transform_values { |balances| balances.sort_by(&:date).last }
+ end
+ end
# Since the query aggregates the *net* of assets - liabilities, this means that if we're looking at
# a single liability account, we'll get a negative set of values. This is not what the user expects
# to see. When favorable direction is "down" (i.e. liability, decrease is "good"), we need to invert
@@ -90,72 +110,75 @@ def sign_multiplier
favorable_direction == "down" ? -1 : 1
end
- def query
- <<~SQL
- WITH dates AS (
- SELECT generate_series(DATE :start_date, DATE :end_date, :interval::interval)::date AS date
- UNION DISTINCT
- SELECT :end_date::date -- Ensure end date is included
- )
- SELECT
- d.date,
- -- Use flows_factor: already handles asset (+1) vs liability (-1)
- COALESCE(SUM(last_bal.end_balance * last_bal.flows_factor * COALESCE(er.rate, 1) * :sign_multiplier::integer), 0) AS end_balance,
- COALESCE(SUM(last_bal.end_cash_balance * last_bal.flows_factor * COALESCE(er.rate, 1) * :sign_multiplier::integer), 0) AS end_cash_balance,
- -- Holdings only for assets (flows_factor = 1)
- COALESCE(SUM(
- CASE WHEN last_bal.flows_factor = 1
- THEN last_bal.end_non_cash_balance
- ELSE 0
- END * COALESCE(er.rate, 1) * :sign_multiplier::integer
- ), 0) AS end_holdings_balance,
- -- Previous balances
- COALESCE(SUM(last_bal.start_balance * last_bal.flows_factor * COALESCE(er.rate, 1) * :sign_multiplier::integer), 0) AS start_balance,
- COALESCE(SUM(last_bal.start_cash_balance * last_bal.flows_factor * COALESCE(er.rate, 1) * :sign_multiplier::integer), 0) AS start_cash_balance,
- COALESCE(SUM(
- CASE WHEN last_bal.flows_factor = 1
- THEN last_bal.start_non_cash_balance
- ELSE 0
- END * COALESCE(er.rate, 1) * :sign_multiplier::integer
- ), 0) AS start_holdings_balance
- FROM dates d
- CROSS JOIN accounts
- LEFT JOIN LATERAL (
- SELECT b.end_balance,
- b.end_cash_balance,
- b.end_non_cash_balance,
- b.start_balance,
- b.start_cash_balance,
- b.start_non_cash_balance,
- b.flows_factor
- FROM balances b
- WHERE b.account_id = accounts.id
- AND b.currency = accounts.currency
- AND b.date <= d.date
- ORDER BY b.date DESC
- LIMIT 1
- ) last_bal ON TRUE
- LEFT JOIN LATERAL (
- SELECT COALESCE(
- (SELECT er.rate
- FROM exchange_rates er
- WHERE er.from_currency = accounts.currency
- AND er.to_currency = :target_currency
- AND er.date <= d.date
- ORDER BY er.date DESC
- LIMIT 1),
- (SELECT er.rate
- FROM exchange_rates er
- WHERE er.from_currency = accounts.currency
- AND er.to_currency = :target_currency
- AND er.date > d.date
- ORDER BY er.date ASC
- LIMIT 1)
- ) AS rate
- ) er ON TRUE
- WHERE accounts.id = ANY(array[:account_ids]::uuid[])
- GROUP BY d.date
- ORDER BY d.date
- SQL
+ def rate_for(from, to, date)
+ if from == to
+ return 1
+ end
+ rates = exchange_rates.dig([ from, to ]) || []
+ closest_rate = rates.bsearch { |rate| rate.date <= date }
+ closest_rate&.rate || 1
+ end
+
+ def query_data
+ @query_data ||= begin
+ result = date_series.map do |date|
+ OpenStruct.new(
+ date: date,
+ end_balance: 0,
+ end_cash_balance: 0,
+ end_holdings_balance: 0,
+ start_balance: 0,
+ start_cash_balance: 0,
+ start_holdings_balance: 0
+ )
+ end
+ accounts.each do |account|
+ previous = starting_balances.dig(account.id)
+ date_series.map.with_index.each do |date, index|
+ balance = balances.dig([ account.id, date ]) || previous
+ previous = balance
+ rate = rate_for(account.currency, currency, date)
+ if balance
+ factor = balance.flows_factor * sign_multiplier * rate
+ result[index].end_balance += balance.end_balance * factor
+ result[index].end_cash_balance += balance.end_cash_balance * factor
+ result[index].start_balance += balance.start_balance * factor
+ result[index].start_cash_balance += balance.start_cash_balance * factor
+ if balance.flows_factor == 1
+ result[index].end_holdings_balance += balance.end_non_cash_balance * factor
+ result[index].start_holdings_balance += balance.start_non_cash_balance * factor
+ end
+ end
+ end
+ end
+ result
+ end
+ rescue => e
+ Rails.logger.error "Query data error: #{e.message} for accounts #{account_ids}, period #{period.start_date} to #{period.end_date}"
+ raise
+ end
+
+ def date_series
+ @date_series ||= begin
+ dates = []
+ current_date = period.start_date
+
+ while current_date <= period.end_date
+ dates << current_date
+ current_date = case interval
+ when "1 day"
+ current_date + 1.day
+ when "1 week"
+ current_date + 1.week
+ else
+ # Default to daily if interval is not recognized
+ current_date + 1.day
+ end
+ end
+
+ # Ensure end date is included
+ dates << period.end_date unless dates.include?(period.end_date)
+ dates.sort
+ end
end
end
diff --git a/app/services/api_rate_limiter.rb b/app/services/api_rate_limiter.rb
deleted file mode 100644
index d3a771cf5c9..00000000000
--- a/app/services/api_rate_limiter.rb
+++ /dev/null
@@ -1,95 +0,0 @@
-class ApiRateLimiter
- # Rate limit tiers (requests per hour)
- RATE_LIMITS = {
- standard: 100,
- premium: 1000,
- enterprise: 10000
- }.freeze
-
- DEFAULT_TIER = :standard
-
- def initialize(api_key)
- @api_key = api_key
- @redis = Redis.new
- end
-
- # Check if the API key has exceeded its rate limit
- def rate_limit_exceeded?
- current_count >= rate_limit
- end
-
- # Increment the request count for this API key
- def increment_request_count!
- key = redis_key
- current_time = Time.current.to_i
- window_start = (current_time / 3600) * 3600 # Hourly window
-
- @redis.multi do |transaction|
- # Use a sliding window with hourly buckets
- transaction.hincrby(key, window_start.to_s, 1)
- transaction.expire(key, 7200) # Keep data for 2 hours to handle sliding window
- end
- end
-
- # Get current request count within the current hour
- def current_count
- key = redis_key
- current_time = Time.current.to_i
- window_start = (current_time / 3600) * 3600
-
- count = @redis.hget(key, window_start.to_s)
- count.to_i
- end
-
- # Get the rate limit for this API key's tier
- def rate_limit
- tier = determine_tier
- RATE_LIMITS[tier]
- end
-
- # Calculate seconds until the rate limit resets
- def reset_time
- current_time = Time.current.to_i
- next_window = ((current_time / 3600) + 1) * 3600
- next_window - current_time
- end
-
- # Get detailed usage information
- def usage_info
- {
- current_count: current_count,
- rate_limit: rate_limit,
- remaining: [ rate_limit - current_count, 0 ].max,
- reset_time: reset_time,
- tier: determine_tier
- }
- end
-
- # Class method to get usage for an API key without incrementing
- def self.usage_for(api_key)
- limit(api_key).usage_info
- end
-
- def self.limit(api_key)
- if Rails.application.config.app_mode.self_hosted?
- # Use NoopApiRateLimiter for self-hosted mode
- # This means no rate limiting is applied
- NoopApiRateLimiter.new(api_key)
- else
- new(api_key)
- end
- end
-
- private
-
- def redis_key
- "api_rate_limit:#{@api_key.id}"
- end
-
- def determine_tier
- # For now, all API keys are standard tier
- # This can be extended later to support different tiers based on user subscription
- # or API key configuration
- DEFAULT_TIER
- end
-end
diff --git a/app/services/noop_api_rate_limiter.rb b/app/services/noop_api_rate_limiter.rb
deleted file mode 100644
index 116b6537ed8..00000000000
--- a/app/services/noop_api_rate_limiter.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-class NoopApiRateLimiter
- def initialize(api_key)
- @api_key = api_key
- end
-
- def rate_limit_exceeded?
- false
- end
-
- def increment_request_count!
- # No operation
- end
-
- def current_count
- 0
- end
-
- def rate_limit
- Float::INFINITY
- end
-
- def reset_time
- 0
- end
-
- def usage_info
- {
- current_count: 0,
- rate_limit: Float::INFINITY,
- remaining: Float::INFINITY,
- reset_time: 0,
- tier: :noop
- }
- end
-
- def self.usage_for(api_key)
- new(api_key).usage_info
- end
-end
diff --git a/bin/ci b/bin/ci
new file mode 100755
index 00000000000..4137ad5bb07
--- /dev/null
+++ b/bin/ci
@@ -0,0 +1,6 @@
+#!/usr/bin/env ruby
+require_relative "../config/boot"
+require "active_support/continuous_integration"
+
+CI = ActiveSupport::ContinuousIntegration
+require_relative "../config/ci.rb"
diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint
index 67ef493142a..9873346d6b9 100755
--- a/bin/docker-entrypoint
+++ b/bin/docker-entrypoint
@@ -2,6 +2,7 @@
# If running the rails server then create or migrate existing database
if [ "${1}" == "./bin/rails" ] && [ "${2}" == "server" ]; then
+ ./bin/rails litestream:restore -- --database=storage/production_primary.sqlite3 --if-db-not-exists
./bin/rails db:prepare
fi
diff --git a/bin/jobs b/bin/jobs
new file mode 100755
index 00000000000..dcf59f309ae
--- /dev/null
+++ b/bin/jobs
@@ -0,0 +1,6 @@
+#!/usr/bin/env ruby
+
+require_relative "../config/environment"
+require "solid_queue/cli"
+
+SolidQueue::Cli.start(ARGV)
diff --git a/bin/rubocop b/bin/rubocop
index 40330c0ff1c..5a20504716c 100755
--- a/bin/rubocop
+++ b/bin/rubocop
@@ -2,7 +2,7 @@
require "rubygems"
require "bundler/setup"
-# explicit rubocop config increases performance slightly while avoiding config confusion.
+# Explicit RuboCop config increases performance slightly while avoiding config confusion.
ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__))
load Gem.bin_path("rubocop", "rubocop")
diff --git a/bin/setup b/bin/setup
index 83098e2485e..39ccc6245d6 100755
--- a/bin/setup
+++ b/bin/setup
@@ -1,7 +1,6 @@
#!/usr/bin/env ruby
require "fileutils"
-# path to your application root.
APP_ROOT = File.expand_path("..", __dir__)
def system!(*args)
@@ -14,7 +13,6 @@ FileUtils.chdir APP_ROOT do
# Add necessary setup steps to this file.
puts "== Installing dependencies =="
- system! "gem install bundler --conservative"
system("bundle check") || system!("bundle install")
puts "\n== Building design tokens =="
@@ -28,10 +26,14 @@ FileUtils.chdir APP_ROOT do
puts "\n== Preparing database =="
system! "bin/rails db:prepare"
+ system! "bin/rails db:reset" if ARGV.include?("--reset")
puts "\n== Removing old logs and tempfiles =="
system! "bin/rails log:clear tmp:clear"
- puts "\n== Restarting application server =="
- system! "bin/rails restart"
+ unless ARGV.include?("--skip-server")
+ puts "\n== Starting development server =="
+ STDOUT.flush # flush the output before exec(2) so that it displays
+ exec "bin/dev"
+ end
end
diff --git a/compose.example.yml b/compose.example.yml
index 108fd6b9a8b..d39b1770681 100644
--- a/compose.example.yml
+++ b/compose.example.yml
@@ -42,11 +42,6 @@
# Or use explicit DNS servers that prefer IPv4 (already configured below).
#
-x-db-env: &db_env
- POSTGRES_USER: ${POSTGRES_USER:-sure_user}
- POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-sure_password}
- POSTGRES_DB: ${POSTGRES_DB:-sure_production}
-
x-rails-env: &rails_env
<<: *db_env
SECRET_KEY_BASE: ${SECRET_KEY_BASE:-a7523c3d0ae56415046ad8abae168d71074a79534a7062258f8d1d51ac2f76d3c3bc86d86b6b0b307df30d9a6a90a2066a3fa9e67c5e6f374dbd7dd4e0778e13}
@@ -55,8 +50,7 @@ x-rails-env: &rails_env
RAILS_ASSUME_SSL: "false"
DB_HOST: db
DB_PORT: 5432
- REDIS_URL: redis://redis:6379/1
- # NOTE: enabling OpenAI will incur costs when you use AI-related features in the app (chat, rules). Make sure you have set appropriate spend limits on your account before adding this.
+# NOTE: enabling OpenAI will incur costs when you use AI-related features in the app (chat, rules). Make sure you have set appropriate spend limits on your account before adding this.
OPENAI_ACCESS_TOKEN: ${OPENAI_ACCESS_TOKEN}
services:
@@ -77,86 +71,11 @@ services:
depends_on:
db:
condition: service_healthy
- redis:
- condition: service_healthy
- dns:
- - 8.8.8.8
- - 1.1.1.1
- networks:
- - sure_net
-
- worker:
- image: ghcr.io/we-promise/sure:stable
- command: bundle exec sidekiq
- volumes:
- - app-storage:/rails/storage
- restart: unless-stopped
- depends_on:
- db:
- condition: service_healthy
- redis:
- condition: service_healthy
- dns:
- - 8.8.8.8
- - 1.1.1.1
- environment:
- <<: *rails_env
- networks:
- - sure_net
-
- db:
- image: postgres:16
- restart: unless-stopped
- volumes:
- - postgres-data:/var/lib/postgresql/data
- environment:
- <<: *db_env
- healthcheck:
- test: [ "CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" ]
- interval: 5s
- timeout: 5s
- retries: 5
- networks:
- - sure_net
-
- backup:
- profiles:
- - backup
- image: prodrigestivill/postgres-backup-local
- restart: unless-stopped
- volumes:
- - /opt/sure-data/backups:/backups # Change this path to your desired backup location on the host machine
- environment:
- - POSTGRES_HOST=db
- - POSTGRES_DB=${POSTGRES_DB:-sure_production}
- - POSTGRES_USER=${POSTGRES_USER:-sure_user}
- - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-sure_password} # pipelock:ignore
- - SCHEDULE=@daily # Runs once a day at midnight
- - BACKUP_KEEP_DAYS=7 # Keeps the last 7 days of backups
- - BACKUP_KEEP_WEEKS=4 # Keeps 4 weekly backups
- - BACKUP_KEEP_MONTHS=6 # Keeps 6 monthly backups
- depends_on:
- - db
- networks:
- - sure_net
-
- redis:
- image: redis:latest
- restart: unless-stopped
- volumes:
- - redis-data:/data
- healthcheck:
- test: [ "CMD", "redis-cli", "ping" ]
- interval: 5s
- timeout: 5s
- retries: 5
networks:
- sure_net
volumes:
app-storage:
- postgres-data:
- redis-data:
networks:
sure_net:
diff --git a/config/application.rb b/config/application.rb
index 1269f1aad08..4db15eeb251 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -9,7 +9,7 @@
module Sure
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
- config.load_defaults 7.2
+ config.load_defaults 8.1
# Please, add to the `ignore` list any other `lib` subdirectories that do
# not contain `.rb` files, or that should not be reloaded or eager loaded.
@@ -34,10 +34,12 @@ class Application < Rails::Application
config.active_record.encryption = Rails.application.credentials.active_record_encryption
end
- config.view_component.preview_controller = "LookbooksController"
- config.lookbook.preview_display_options = {
- theme: [ "light", "dark" ] # available in view as params[:theme]
- }
+ if Rails.env.development?
+ config.view_component.preview_controller = "LookbooksController"
+ config.lookbook.preview_display_options = {
+ theme: [ "light", "dark" ] # available in view as params[:theme]
+ }
+ end
# Enable Skylight instrumentation for ActiveJob (background workers)
config.skylight.probes << "active_job" if defined?(Skylight)
@@ -45,11 +47,8 @@ class Application < Rails::Application
# Enable Rack::Attack middleware for API rate limiting
config.middleware.use Rack::Attack
- config.x.ui = ActiveSupport::OrderedOptions.new
- default_layout = ENV.fetch("DEFAULT_UI_LAYOUT", "dashboard")
- config.x.ui.default_layout = default_layout.in?(%w[dashboard intro]) ? default_layout : "dashboard"
- # Handle OmniAuth/OIDC errors gracefully (must be before OmniAuth middleware)
- require_relative "../app/middleware/omniauth_error_handler"
- config.middleware.use OmniauthErrorHandler
+ config.mission_control.jobs.http_basic_auth_enabled = false if Rails.env.development?
+ MissionControl::Jobs.http_basic_auth_user = "jobs" if Rails.env.production?
+ MissionControl::Jobs.http_basic_auth_password = ENV["MISSION_CONTROL_JOBS_PASSWORD"] || "jobs" if Rails.env.production?
end
end
diff --git a/config/cable.yml b/config/cable.yml
index fc5a1eb8efa..7ca155ef873 100644
--- a/config/cable.yml
+++ b/config/cable.yml
@@ -1,10 +1,21 @@
+# Async adapter only works within the same process, so for manually triggering cable updates from a console,
+# and seeing results in the browser, you must do so from the web console (running inside the dev process),
+# not a terminal started via bin/rails console! Add "console" to any action or any ERB template view
+# to make the web console appear.
+
+default: &default
+ adapter: solid_cable
+ connects_to:
+ database:
+ writing: cable
+ polling_interval: 0.1.seconds
+ message_retention: 1.day
+
development:
- adapter: async
+ <<: *default
test:
adapter: test
production:
- adapter: redis
- url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
- channel_prefix: sure_production
+ <<: *default
diff --git a/config/cache.yml b/config/cache.yml
new file mode 100644
index 00000000000..946125a096f
--- /dev/null
+++ b/config/cache.yml
@@ -0,0 +1,17 @@
+default: &default
+ store_options:
+ # Cap age of oldest cache entry to fulfill retention policies
+ max_age: <%= 1.hour.to_i %>
+ max_size: <%= 1.gigabytes %>
+ namespace: <%= Rails.env %>
+
+development: &development
+ <<: *default
+ database: cache
+
+test:
+ <<: *default
+
+production: &production
+ <<: *default
+ database: cache
diff --git a/config/ci.rb b/config/ci.rb
new file mode 100644
index 00000000000..795f52c7b20
--- /dev/null
+++ b/config/ci.rb
@@ -0,0 +1,22 @@
+# Run using bin/ci
+
+CI.run do
+ step "Setup", "bin/setup --skip-server"
+
+ step "Style: Ruby", "bin/rubocop"
+
+ step "Security: Importmap vulnerability audit", "bin/importmap audit"
+ step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error"
+
+ step "Tests: Rails", "bin/rails test"
+ step "Tests: System", "bin/rails test:system"
+ step "Tests: Seeds", "env RAILS_ENV=test bin/rails db:seed:replant"
+
+ # Optional: set a green GitHub commit status to unblock PR merge.
+ # Requires the `gh` CLI and `gh extension install basecamp/gh-signoff`.
+ # if success?
+ # step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff"
+ # else
+ # failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again."
+ # end
+end
diff --git a/config/database.yml b/config/database.yml
index 5b249238a33..4f6a4ee9534 100644
--- a/config/database.yml
+++ b/config/database.yml
@@ -1,20 +1,50 @@
default: &default
- adapter: postgresql
- encoding: unicode
- pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 3 } %>
- host: <%= ENV.fetch("DB_HOST") { "127.0.0.1" } %>
- port: <%= ENV.fetch("DB_PORT") { "5432" } %>
- user: <%= ENV.fetch("POSTGRES_USER") { nil } %>
- password: <%= ENV.fetch("POSTGRES_PASSWORD") { nil } %>
+ adapter: sqlite3
+ timeout: 5000
+ default_transaction_mode: IMMEDIATE
+ pragmas:
+ foreign_keys: "ON"
development:
- <<: *default
- database: <%= ENV.fetch("POSTGRES_DB") { "sure_development" } %>
+ primary:
+ <<: *default
+ database: storage/development_primary.sqlite3
+ extensions:
+ - <%= SQLean::UUID.to_path %>
+ cache:
+ <<: *default
+ database: storage/development_cache.sqlite3
+ migrations_paths: db/cache_migrate
+ queue:
+ <<: *default
+ database: storage/development_queue.sqlite3
+ migrations_paths: db/queue_migrate
+ cable:
+ <<: *default
+ database: storage/development_cable.sqlite3
+ migrations_paths: db/cable_migrate
test:
<<: *default
- database: <%= ENV.fetch("POSTGRES_DB") { "sure_test" } %>
+ database: storage/test_primary.sqlite3
+ extensions:
+ - <%= SQLean::UUID.to_path %>
production:
- <<: *default
- database: <%= ENV.fetch("POSTGRES_DB") { "sure_production" } %>
+ primary:
+ <<: *default
+ database: storage/production_primary.sqlite3
+ extensions:
+ - <%= SQLean::UUID.to_path %>
+ cache:
+ <<: *default
+ database: storage/production_cache.sqlite3
+ migrations_paths: db/cache_migrate
+ queue:
+ <<: *default
+ database: storage/production_queue.sqlite3
+ migrations_paths: db/queue_migrate
+ cable:
+ <<: *default
+ database: storage/production_cable.sqlite3
+ migrations_paths: db/cable_migrate
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 553da47e3e8..1738775c8df 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -3,9 +3,7 @@
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
- # In the development environment your application's code is reloaded any time
- # it changes. This slows down response time but is perfect for development
- # since you don't have to restart the web server when you make code changes.
+ # Make code changes take effect immediately without server restart.
config.enable_reloading = true
# Do not eager load code on boot.
@@ -14,40 +12,34 @@
# Show full error reports.
config.consider_all_requests_local = true
- # Enable server timing
+ # Enable server timing.
config.server_timing = true
- # Enable/disable caching. By default caching is disabled.
- # Run rails dev:cache to toggle caching.
+ # Enable/disable Action Controller caching. By default Action Controller caching is disabled.
+ # Run rails dev:cache to toggle Action Controller caching.
if Rails.root.join("tmp/caching-dev.txt").exist?
config.action_controller.perform_caching = true
config.action_controller.enable_fragment_cache_logging = true
-
- config.cache_store = :memory_store
- config.public_file_server.headers = {
- "Cache-Control" => "public, max-age=#{2.days.to_i}"
- }
+ config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" }
else
config.action_controller.perform_caching = false
-
- config.cache_store = :null_store
end
- # Store uploaded files on the local file system (see config/storage.yml for options).
- config.active_storage.service = ENV.fetch("ACTIVE_STORAGE_SERVICE", "local").to_sym
- config.after_initialize do
- ActiveStorage::Current.url_options = { host: "localhost", port: 3000 }
- end
+ # Change to :null_store to avoid any caching.
+ config.cache_store = :memory_store
- # Set Active Storage URL expiration time to 7 days
- config.active_storage.urls_expire_in = 7.days
+ # Store uploaded files on the local file system (see config/storage.yml for options).
+ config.active_storage.service = :local
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
config.action_mailer.delivery_method = :letter_opener
+
+ # Make template changes take effect immediately.
config.action_mailer.perform_caching = false
+ # Set localhost to be used by links generated in mailer templates.
config.action_mailer.perform_deliveries = true
config.action_mailer.default_url_options = { host: "localhost", port: ENV.fetch("PORT") { 3000 } }
@@ -55,18 +47,26 @@
# Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log
- # Raise exceptions for disallowed deprecations.
- config.active_support.disallowed_deprecation = :raise
-
- # Tell Active Support which deprecation messages to disallow.
- config.active_support.disallowed_deprecation_warnings = []
-
# Raise an error on page load if there are pending migrations.
config.active_record.migration_error = :page_load
- config.assets.quiet = true
+ # Highlight code that triggered database queries in logs.
config.active_record.verbose_query_logs = true
+
+ # Append comments with runtime information tags to SQL queries in logs.
+ config.active_record.query_log_tags_enabled = true
+
+ # Highlight code that enqueued background job in logs.
config.active_job.verbose_enqueue_logs = true
+ config.active_job.queue_adapter = :solid_queue
+ config.solid_queue.connects_to = { database: { writing: :queue } }
+ config.solid_queue.logger = ActiveSupport::Logger.new(STDOUT)
+
+ # Highlight code that triggered redirect in logs.
+ config.action_dispatch.verbose_redirect_logs = true
+
+ # Suppress logger output for asset requests.
+ config.assets.quiet = true
# Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true
@@ -77,11 +77,11 @@
# Uncomment if you wish to allow Action Cable access from any origin.
# config.action_cable.disable_request_forgery_protection = true
- # Raise error when a before_action's only/except options reference missing actions
+ # Raise error when a before_action's only/except options reference missing actions.
config.action_controller.raise_on_missing_callback_actions = true
# Apply autocorrection by RuboCop to files generated by `bin/rails generate`.
- config.generators.apply_rubocop_autocorrect_after_generate!
+ # config.generators.apply_rubocop_autocorrect_after_generate!
# Allow connection from any host in development
config.hosts = nil
diff --git a/config/environments/production.rb b/config/environments/production.rb
index fc7120a0a95..10f077a6ed9 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -6,47 +6,32 @@
# Code is not reloaded between requests.
config.enable_reloading = false
- # Eager load code on boot. This eager loads most of Rails and
- # your application in memory, allowing both threaded web servers
- # and those relying on copy on write to perform better.
- # Rake tasks automatically ignore this option for performance.
+ # Eager load code on boot for better performance and memory savings (ignored by Rake tasks).
config.eager_load = true
- # Full error reports are disabled and caching is turned on.
+ # Full error reports are disabled.
config.consider_all_requests_local = false
- config.action_controller.perform_caching = true
- # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment
- # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files).
- # config.require_master_key = true
+ # Turn on fragment caching in view templates.
+ config.action_controller.perform_caching = true
- # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead.
- # config.public_file_server.enabled = false
+ # Cache assets for far-future expiry since they are all digest stamped.
+ config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" }
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.asset_host = "http://assets.example.com"
- # Specifies the header that your server uses for sending files.
- # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache
- # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX
-
# Store uploaded files on the local file system (see config/storage.yml for options).
config.active_storage.service = ENV.fetch("ACTIVE_STORAGE_SERVICE", "local").to_sym
- # Set Active Storage URL expiration time to 7 days
- config.active_storage.urls_expire_in = 7.days
-
- # Mount Action Cable outside main process or domain.
- # config.action_cable.mount_path = nil
- # config.action_cable.url = "wss://example.com/cable"
- # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ]
+ # Assume all access to the app is happening through a SSL-terminating reverse proxy.
+ config.assume_ssl = ActiveModel::Type::Boolean.new.cast(ENV.fetch("RAILS_ASSUME_SSL", true))
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
config.force_ssl = ActiveModel::Type::Boolean.new.cast(ENV.fetch("RAILS_FORCE_SSL", true))
- # Assume all access to the app is happening through a SSL-terminating reverse proxy.
- # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies.
- config.assume_ssl = ActiveModel::Type::Boolean.new.cast(ENV.fetch("RAILS_ASSUME_SSL", true))
+ # Skip http-to-https redirect for the default health check endpoint.
+ # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } }
# Log to Logtail if API key is present, otherwise log to STDOUT
base_logger = if ENV["LOGTAIL_API_KEY"].present? && ENV["LOGTAIL_INGESTING_HOST"].present?
@@ -55,60 +40,76 @@
ingesting_host: ENV["LOGTAIL_INGESTING_HOST"]
)
else
- ActiveSupport::Logger.new(STDOUT)
- .tap { |logger| logger.formatter = ::Logger::Formatter.new }
+ ActiveSupport::TaggedLogging.logger(STDOUT)
end
config.logger = ActiveSupport::TaggedLogging.new(base_logger)
-
# Prepend all log lines with the following tags.
config.log_tags = [ :request_id ]
- # "info" includes generic and useful information about system operation, but avoids logging too much
- # information to avoid inadvertent exposure of personally identifiable information (PII). If you
- # want to log everything, set the level to "debug".
+ # Change to "debug" to log everything (including potentially personally-identifiable information!).
config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")
- if ENV["REDIS_URL"].present?
- config.cache_store = :redis_cache_store, { url: ENV["REDIS_URL"] }
- end
+ # Prevent health checks from clogging up the logs.
+ config.silence_healthcheck_path = "/up"
- config.action_mailer.perform_caching = false
- config.action_mailer.deliver_later_queue_name = :high_priority
- config.action_mailer.default_url_options = { host: ENV["APP_DOMAIN"] }
- config.action_mailer.delivery_method = :smtp
- config.action_mailer.smtp_settings = {
- address: ENV["SMTP_ADDRESS"],
- port: ENV["SMTP_PORT"],
- user_name: ENV["SMTP_USERNAME"],
- password: ENV["SMTP_PASSWORD"],
- tls: ENV["SMTP_TLS_ENABLED"] == "true",
- openssl_verify_mode: ENV["SMTP_TLS_SKIP_VERIFY"] == "true" ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER,
- ca_file: ENV["SSL_CA_FILE"]
- }
+ # Don't log any deprecations.
+ config.active_support.report_deprecations = false
+
+ # Replace the default in-process memory cache store with a durable alternative.
+ # config.cache_store = :mem_cache_store
+
+ # Replace the default in-process and non-durable queuing backend for Active Job.
+ # config.active_job.queue_adapter = :resque
# Ignore bad email addresses and do not raise email delivery errors.
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
# config.action_mailer.raise_delivery_errors = false
+ # Set host to be used by links generated in mailer templates.
+ config.action_mailer.default_url_options = { host: "example.com" }
+
+ # Specify outgoing SMTP server. Remember to add smtp/* credentials via bin/rails credentials:edit.
+ # config.action_mailer.smtp_settings = {
+ # user_name: Rails.application.credentials.dig(:smtp, :user_name),
+ # password: Rails.application.credentials.dig(:smtp, :password),
+ # address: "smtp.example.com",
+ # port: 587,
+ # authentication: :plain
+ # }
+
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# the I18n.default_locale when a translation cannot be found).
config.i18n.fallbacks = true
- # Don't log any deprecations.
- config.active_support.report_deprecations = false
-
# Do not dump schema after migrations.
config.active_record.dump_schema_after_migration = false
+ # Only use :id for inspections in production.
+ config.active_record.attributes_for_inspect = [ :id ]
+
# Enable DNS rebinding protection and other `Host` header attacks.
# config.hosts = [
# "example.com", # Allow requests from example.com
# /.*\.example\.com/ # Allow requests from subdomains like `www.example.com`
# ]
+ #
# Skip DNS rebinding protection for the default health check endpoint.
# config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
-
- # set REDIS_URL for Sidekiq to use Redis
- config.active_job.queue_adapter = :sidekiq
+ config.cache_store = :solid_cache_store
+ config.active_job.queue_adapter = :solid_queue
+ config.solid_queue.connects_to = { database: { writing: :queue } }
+ config.action_mailer.perform_caching = false
+ config.action_mailer.deliver_later_queue_name = :high_priority
+ config.action_mailer.default_url_options = { host: ENV["APP_DOMAIN"] }
+ config.action_mailer.delivery_method = :smtp
+ config.action_mailer.smtp_settings = {
+ address: ENV["SMTP_ADDRESS"],
+ port: ENV["SMTP_PORT"],
+ user_name: ENV["SMTP_USERNAME"],
+ password: ENV["SMTP_PASSWORD"],
+ tls: ENV["SMTP_TLS_ENABLED"] == "true",
+ openssl_verify_mode: ENV["SMTP_TLS_SKIP_VERIFY"] == "true" ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER,
+ ca_file: ENV["SSL_CA_FILE"]
+ }
end
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 362e5286443..431e076a847 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -17,11 +17,9 @@
# loading is working properly before deploying your code.
config.eager_load = ENV["CI"].present?
- # Configure public file server for tests with Cache-Control for performance.
+ # Configure public file server for tests with cache-control for performance.
config.public_file_server.enabled = true
- config.public_file_server.headers = {
- "Cache-Control" => "public, max-age=#{1.hour.to_i}"
- }
+ config.public_file_server.headers = { "cache-control" => "public, max-age=3600" }
# Set default sender email for tests
ENV["EMAIL_SENDER"] = "hello@example.com"
@@ -40,13 +38,14 @@
# Store uploaded files on the local file system in a temporary directory.
config.active_storage.service = :test
- config.action_mailer.perform_caching = false
-
# Tell Action Mailer not to deliver emails to the real world.
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
+ # Set host to be used by links generated in mailer templates.
+ config.action_mailer.default_url_options = { host: "example.com" }
+
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
@@ -64,7 +63,7 @@
# Annotate rendered view with file names.
# config.action_view.annotate_rendered_view_with_filenames = true
- # Raise error when a before_action's only/except options reference missing actions
+ # Raise error when a before_action's only/except options reference missing actions.
config.action_controller.raise_on_missing_callback_actions = true
config.active_record.encryption.primary_key = "test"
diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb
index d06e93b0970..a8687e82c3f 100644
--- a/config/initializers/assets.rb
+++ b/config/initializers/assets.rb
@@ -4,5 +4,6 @@
Rails.application.config.assets.version = "1.0"
# Add additional assets to the asset load path.
+# Rails.application.config.assets.paths << Emoji.images_path
Rails.application.config.assets.paths << "app/components"
Rails.application.config.importmap.cache_sweepers << Rails.root.join("app/components")
diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb
index 834aa21186d..d1642495adc 100644
--- a/config/initializers/content_security_policy.rb
+++ b/config/initializers/content_security_policy.rb
@@ -21,6 +21,10 @@
# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
# config.content_security_policy_nonce_directives = %w(script-src style-src)
#
+# # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag`
+# # if the corresponding directives are specified in `content_security_policy_nonce_directives`.
+# # config.content_security_policy_nonce_auto = true
+#
# # Report violations without enforcing the policy.
# # config.content_security_policy_report_only = true
# end
diff --git a/config/initializers/enable_yjit.rb b/config/initializers/enable_yjit.rb
index 8a442216b26..8bdf252ead1 100644
--- a/config/initializers/enable_yjit.rb
+++ b/config/initializers/enable_yjit.rb
@@ -1,10 +1,6 @@
-# Automatically enable YJIT as of Ruby 3.3, as it brings very
-# sizeable performance improvements.
-
-# If you are deploying to a memory constrained environment
-# you may want to delete this file, but otherwise it's free
-# performance.
-if defined? RubyVM::YJIT.enable
+# Enable YJIT only when explicitly requested. It can improve throughput,
+# but it also increases memory usage on smaller hosts.
+if ENV["ENABLE_YJIT"] == "true" && defined?(RubyVM::YJIT.enable)
Rails.application.config.after_initialize do
RubyVM::YJIT.enable
end
diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb
index af9db03123f..f6028485d87 100644
--- a/config/initializers/filter_parameter_logging.rb
+++ b/config/initializers/filter_parameter_logging.rb
@@ -4,6 +4,5 @@
# Use this to limit dissemination of sensitive information.
# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.
Rails.application.config.filter_parameters += [
- :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :openai_access_token,
- :client_id, :consumer_key, :snaptrade_user_id, :snaptrade_user_secret
+ :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc, :openai_access_token
]
diff --git a/config/initializers/generator.rb b/config/initializers/generator.rb
index 034fab6e565..1111e2319a3 100644
--- a/config/initializers/generator.rb
+++ b/config/initializers/generator.rb
@@ -1,3 +1,3 @@
Rails.application.config.generators do |g|
- g.orm :active_record, primary_key_type: :uuid
+ g.orm :active_record, primary_key_type: :string
end
diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb
index 8918e6f7699..19be50525fe 100644
--- a/config/initializers/rack_attack.rb
+++ b/config/initializers/rack_attack.rb
@@ -1,6 +1,11 @@
# frozen_string_literal: true
class Rack::Attack
+ SCANNER_PATH_PATTERNS = [
+ %r{(?:^|/)[^/]+\.php\d*\z}i,
+ %r{\A/+(?:wp-|wordpress|xmlrpc\.php|wp-admin|wp-content|wp-includes|plugin-(?:install|editor)\.php)}i
+ ].freeze
+
# Enable Rack::Attack only in production and staging (disable in test/development to avoid rate-limit flakiness)
enabled = Rails.env.production? || Rails.env.staging?
self.enabled = enabled
@@ -58,7 +63,11 @@ class Rack::Attack
]
user_agent = request.user_agent
- suspicious_user_agents.any? { |pattern| user_agent =~ pattern } if user_agent
+ suspicious_user_agent = user_agent && suspicious_user_agents.any? { |pattern| user_agent =~ pattern }
+ normalized_path = request.path.sub(/\A\/+/, "/")
+ suspicious_path = SCANNER_PATH_PATTERNS.any? { |pattern| normalized_path.match?(pattern) }
+
+ suspicious_user_agent || suspicious_path
end
# Configure response for throttled requests
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
deleted file mode 100644
index 1338491a4f0..00000000000
--- a/config/initializers/sidekiq.rb
+++ /dev/null
@@ -1,78 +0,0 @@
-require "sidekiq/web"
-
-if Rails.env.production?
- Sidekiq::Web.use(Rack::Auth::Basic) do |username, password|
- configured_username = ::Digest::SHA256.hexdigest(ENV.fetch("SIDEKIQ_WEB_USERNAME", "sure"))
- configured_password = ::Digest::SHA256.hexdigest(ENV.fetch("SIDEKIQ_WEB_PASSWORD", "sure"))
-
- ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(username), configured_username) &
- ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(password), configured_password)
- end
-end
-
-# Configure Redis connection for Sidekiq
-# Supports both Redis Sentinel (for HA) and direct Redis URL
-redis_config = if ENV["REDIS_SENTINEL_HOSTS"].present?
- # Redis Sentinel configuration for high availability
- # REDIS_SENTINEL_HOSTS should be comma-separated list: "host1:port1,host2:port2,host3:port3"
- sentinels = ENV["REDIS_SENTINEL_HOSTS"].split(",").filter_map do |host_port|
- parts = host_port.strip.split(":", 2)
- host = parts[0]&.strip
- port_str = parts[1]&.strip
-
- next if host.blank?
-
- # Parse port with validation, default to 26379 if invalid or missing
- port = if port_str.present?
- port_int = port_str.to_i
- (port_int > 0 && port_int <= 65535) ? port_int : 26379
- else
- 26379
- end
-
- { host: host, port: port }
- end
-
- if sentinels.empty?
- Rails.logger.warn("REDIS_SENTINEL_HOSTS is set but no valid sentinel hosts found, falling back to REDIS_URL")
- { url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0") }
- else
- {
- url: "redis://#{ENV.fetch('REDIS_SENTINEL_MASTER', 'mymaster')}/0",
- sentinels: sentinels,
- password: ENV["REDIS_PASSWORD"],
- sentinel_username: ENV.fetch("REDIS_SENTINEL_USERNAME", "default"),
- sentinel_password: ENV["REDIS_PASSWORD"],
- role: :master,
- # Recommended timeouts for Sentinel
- connect_timeout: 0.2,
- read_timeout: 1,
- write_timeout: 1,
- reconnect_attempts: 3
- }
- end
-else
- # Standard Redis URL configuration (no Sentinel)
- { url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0") }
-end
-
-Sidekiq.configure_server do |config|
- config.redis = redis_config
-
- # Initialize auto-sync scheduler when Sidekiq server starts
- config.on(:startup) do
- AutoSyncScheduler.sync!
- Rails.logger.info("[AutoSyncScheduler] Initialized sync_all_accounts cron job")
- rescue => e
- Rails.logger.error("[AutoSyncScheduler] Failed to initialize: #{e.message}")
- end
-end
-
-Sidekiq.configure_client do |config|
- config.redis = redis_config
-end
-
-Sidekiq::Cron.configure do |config|
- # 10 min "catch-up" window in case worker process is re-deploying when cron tick occurs
- config.reschedule_grace_period = 600
-end
diff --git a/config/puma.rb b/config/puma.rb
index 04022936aa1..ecdcdbd34b3 100644
--- a/config/puma.rb
+++ b/config/puma.rb
@@ -7,9 +7,14 @@
# Puma starts a configurable number of processes (workers) and each process
# serves each request in a thread from an internal thread pool.
#
+# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You
+# should only set this value when you want to run 2 or more workers. The
+# default is already 1. You can set it to `auto` to automatically start a worker
+# for each available processor.
+#
# The ideal number of threads per worker depends both on how much time the
# application spends waiting for IO operations and on how much you wish to
-# to prioritize throughput over latency.
+# prioritize throughput over latency.
#
# As a rule of thumb, increasing the number of threads will increase how much
# traffic a given process can handle (throughput), but due to CRuby's
@@ -22,7 +27,7 @@
# Any libraries that use a connection pool or another resource pool should
# be configured to provide at least as many connections as the number of
# threads. This includes Active Record's `pool` parameter in `database.yml`.
-threads_count = ENV.fetch("RAILS_MAX_THREADS") { 3 }
+threads_count = ENV.fetch("RAILS_MAX_THREADS", 3)
threads threads_count, threads_count
if rails_env == "production"
@@ -39,9 +44,7 @@
end
# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
-# The bind host is controlled via the Rails-native `BINDING` env var (set to
-# `0.0.0.0` in containers, or `::` for IPv6 dual-stack). See docs/hosting/docker.md.
-port ENV.fetch("PORT") { 3000 }
+port ENV.fetch("PORT", 3000)
# Specifies the `environment` that Puma will run in.
environment rails_env
@@ -49,6 +52,13 @@
# Allow puma to be restarted by `bin/rails restart` command.
plugin :tmp_restart
+# Run the Solid Queue supervisor inside of Puma for single-server deployments
+plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] || rails_env == "development"
+litestream_enabled = ENV["LITESTREAM_ENABLED"] == "true" && rails_env == "production"
+plugin :litestream if litestream_enabled
+
+# Specify the PID file. Defaults to tmp/pids/server.pid in development.
+# In other environments, only set the PID file if requested.
pidfile ENV["PIDFILE"] if ENV["PIDFILE"]
if rails_env == "development"
diff --git a/config/queue.yml b/config/queue.yml
new file mode 100644
index 00000000000..32ac7ef40b5
--- /dev/null
+++ b/config/queue.yml
@@ -0,0 +1,18 @@
+default: &default
+ dispatchers:
+ - polling_interval: 1
+ batch_size: 500
+ workers:
+ - queues: "*"
+ threads: 2
+ processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %>
+ polling_interval: 0.1
+
+development:
+ <<: *default
+
+test:
+ <<: *default
+
+production:
+ <<: *default
diff --git a/config/recurring.yml b/config/recurring.yml
new file mode 100644
index 00000000000..d1459b4f31c
--- /dev/null
+++ b/config/recurring.yml
@@ -0,0 +1,35 @@
+default: &default
+ clear_solid_queue_finished_jobs:
+ command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)"
+ schedule: every hour at minute 12
+
+ import_market_data:
+ class: "ImportMarketDataJob"
+ queue: "scheduled"
+ args: [{ mode: 'full', clear_cache: false }]
+ schedule: "0 22 * * 1-5" # 5:00 PM EST / 6:00 PM EDT (NY time) Monday through Friday
+ description: "Imports market data daily at 5:00 PM EST (1 hour after market close)"
+
+
+production:
+ <<: *default
+ clean_syncs:
+ schedule: "0 * * * *" # every hour
+ class: "SyncCleanerJob"
+ queue: "scheduled"
+ description: "Cleans up stale syncs"
+
+ run_security_health_checks:
+ schedule: "0 2 * * 1-5" # 2:00 AM EST / 3:00 AM EDT (NY time) Monday through Friday
+ class: "SecurityHealthCheckJob"
+ queue: "scheduled"
+ description: "Runs security health checks to detect issues with security data"
+
+ seed_dump:
+ class: "SeedDumpJob"
+ queue: "scheduled"
+ description: "Runs seed dump manually"
+ schedule: "every month"
+
+development:
+ <<: *default
\ No newline at end of file
diff --git a/config/routes.rb b/config/routes.rb
index 11105021b0e..48116d5c492 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -123,7 +123,7 @@
delete :disable
end
- mount Lookbook::Engine, at: "/design-system"
+ mount Lookbook::Engine, at: "/design-system" if Rails.env.development?
if Rails.env.development?
mount Rswag::Api::Engine => "/api-docs"
diff --git a/config/schedule.yml b/config/schedule.yml
deleted file mode 100644
index c3903a22921..00000000000
--- a/config/schedule.yml
+++ /dev/null
@@ -1,44 +0,0 @@
-import_market_data:
- cron: "0 22 * * 1-5" # 5:00 PM EST / 6:00 PM EDT (NY time) Monday through Friday
- class: "ImportMarketDataJob"
- queue: "scheduled"
- description: "Imports market data daily at 5:00 PM EST (1 hour after market close)"
- args:
- mode: "full"
- clear_cache: false
-
-clean_syncs:
- cron: "0 * * * *" # every hour
- class: "SyncCleanerJob"
- queue: "scheduled"
- description: "Cleans up stale syncs"
-
-run_security_health_checks:
- cron: "0 2 * * 1-5" # 2:00 AM EST / 3:00 AM EDT (NY time) Monday through Friday
- class: "SecurityHealthCheckJob"
- queue: "scheduled"
- description: "Runs security health checks to detect issues with security data"
-
-sync_hourly:
- cron: "0 * * * *" # every hour at the top of the hour
- class: "SyncHourlyJob"
- queue: "scheduled"
- description: "Syncs provider items that opt-in to hourly syncing"
-
-clean_data:
- cron: "0 3 * * *" # daily at 3:00 AM
- class: "DataCleanerJob"
- queue: "scheduled"
- description: "Cleans up old data (e.g., expired merchant associations, expired archived exports)"
-
-clean_inactive_families:
- cron: "0 4 * * *" # daily at 4:00 AM
- class: "InactiveFamilyCleanerJob"
- queue: "scheduled"
- description: "Archives and destroys families that expired their trial without subscribing (managed mode only)"
-
-refresh_demo_family:
- cron: "0 5 * * *" # daily at 5:00 AM UTC
- class: "DemoFamilyRefreshJob"
- queue: "scheduled"
- description: "Refreshes demo family data and emails super admins with daily usage summary"
diff --git a/config/sidekiq.yml b/config/sidekiq.yml
deleted file mode 100644
index 4fce6f00b5e..00000000000
--- a/config/sidekiq.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-concurrency: <%= ENV.fetch("RAILS_MAX_THREADS") { 3 } %>
-queues:
- - [scheduled, 10] # For cron-like jobs (e.g. "daily market data sync")
- - [high_priority, 4]
- - [medium_priority, 2]
- - [low_priority, 1]
- - [default, 1]
diff --git a/db/cable_schema.rb b/db/cable_schema.rb
new file mode 100644
index 00000000000..3aefc381eec
--- /dev/null
+++ b/db/cable_schema.rb
@@ -0,0 +1,23 @@
+# This file is auto-generated from the current state of the database. Instead
+# of editing this file, please use the migrations feature of Active Record to
+# incrementally modify your database, and then regenerate this schema definition.
+#
+# This file is the source Rails uses to define your schema when running `bin/rails
+# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
+# be faster and is potentially less error prone than running all of your
+# migrations from scratch. Old migrations may fail to apply correctly if those
+# migrations use external dependencies or application code.
+#
+# It's strongly recommended that you check this file into your version control system.
+
+ActiveRecord::Schema[8.1].define(version: 1) do
+ create_table "solid_cable_messages", force: :cascade do |t|
+ t.binary "channel", limit: 1024, null: false
+ t.integer "channel_hash", limit: 8, null: false
+ t.datetime "created_at", null: false
+ t.binary "payload", limit: 536870912, null: false
+ t.index ["channel"], name: "index_solid_cable_messages_on_channel"
+ t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash"
+ t.index ["created_at"], name: "index_solid_cable_messages_on_created_at"
+ end
+end
diff --git a/db/cache_schema.rb b/db/cache_schema.rb
new file mode 100644
index 00000000000..2016467a112
--- /dev/null
+++ b/db/cache_schema.rb
@@ -0,0 +1,24 @@
+# This file is auto-generated from the current state of the database. Instead
+# of editing this file, please use the migrations feature of Active Record to
+# incrementally modify your database, and then regenerate this schema definition.
+#
+# This file is the source Rails uses to define your schema when running `bin/rails
+# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
+# be faster and is potentially less error prone than running all of your
+# migrations from scratch. Old migrations may fail to apply correctly if those
+# migrations use external dependencies or application code.
+#
+# It's strongly recommended that you check this file into your version control system.
+
+ActiveRecord::Schema[8.1].define(version: 1) do
+ create_table "solid_cache_entries", force: :cascade do |t|
+ t.integer "byte_size", limit: 4, null: false
+ t.datetime "created_at", null: false
+ t.binary "key", limit: 1024, null: false
+ t.integer "key_hash", limit: 8, null: false
+ t.binary "value", limit: 536870912, null: false
+ t.index ["byte_size"], name: "index_solid_cache_entries_on_byte_size"
+ t.index ["key_hash", "byte_size"], name: "index_solid_cache_entries_on_key_hash_and_byte_size"
+ t.index ["key_hash"], name: "index_solid_cache_entries_on_key_hash", unique: true
+ end
+end
diff --git a/db/migrate/20250924124734_change_uuid_to_string.rb b/db/migrate/20250924124734_change_uuid_to_string.rb
new file mode 100644
index 00000000000..f0b12808152
--- /dev/null
+++ b/db/migrate/20250924124734_change_uuid_to_string.rb
@@ -0,0 +1,328 @@
+class ChangeUuidToString < ActiveRecord::Migration[7.2]
+ def change
+ # remove foreign key constraints
+ remove_all_foreign_keys
+
+ # accounts
+ change_column :accounts, :id, :string, null: false
+ change_column :accounts, :family_id, :string, null: false
+ change_column :accounts, :accountable_id, :string
+ change_column :accounts, :import_id, :string
+ change_column :accounts, :plaid_account_id, :string
+ change_column :accounts, :simplefin_account_id, :string
+
+ # active_storage_attachments
+ change_column :active_storage_attachments, :id, :string, null: false
+ change_column :active_storage_attachments, :record_id, :string, null: false
+ change_column :active_storage_attachments, :blob_id, :string, null: false
+
+ # active_storage_blobs
+ change_column :active_storage_blobs, :id, :string, null: false
+
+ # active_storage_variant_records
+ change_column :active_storage_variant_records, :id, :string, null: false
+ change_column :active_storage_variant_records, :blob_id, :string, null: false
+
+ # addresses
+ change_column :addresses, :id, :string, null: false
+ change_column :addresses, :addressable_id, :string
+
+ # api_keys
+ change_column :api_keys, :id, :string, null: false
+ change_column :api_keys, :user_id, :string, null: false
+
+ # balances
+ change_column :balances, :id, :string, null: false
+ change_column :balances, :account_id, :string, null: false
+
+ # budget_categories
+ change_column :budget_categories, :id, :string, null: false
+ change_column :budget_categories, :budget_id, :string, null: false
+ change_column :budget_categories, :category_id, :string, null: false
+
+ # budgets
+ change_column :budgets, :id, :string, null: false
+ change_column :budgets, :family_id, :string, null: false
+
+ # categories
+ change_column :categories, :id, :string, null: false
+ change_column :categories, :family_id, :string, null: false
+ change_column :categories, :parent_id, :string
+
+ # chats
+ change_column :chats, :id, :string, null: false
+ change_column :chats, :user_id, :string, null: false
+ change_column :chats, :title, :string, null: false
+
+ # credit_cards
+ change_column :credit_cards, :id, :string, null: false
+ # cryptos
+ change_column :cryptos, :id, :string, null: false
+
+ # data_enrichments
+ change_column :data_enrichments, :id, :string, null: false
+ change_column :data_enrichments, :enrichable_id, :string, null: false
+ # depositories
+ change_column :depositories, :id, :string, null: false
+ # entries
+ change_column :entries, :id, :string, null: false
+ change_column :entries, :account_id, :string, null: false
+ change_column :entries, :entryable_id, :string
+ change_column :entries, :import_id, :string
+ # exchange_rates
+ change_column :exchange_rates, :id, :string, null: false
+ # families
+ change_column :families, :id, :string, null: false
+ # family_exports
+ change_column :family_exports, :id, :string, null: false
+ change_column :family_exports, :family_id, :string, null: false
+ # holdings
+ change_column :holdings, :id, :string, null: false
+ change_column :holdings, :account_id, :string, null: false
+ change_column :holdings, :security_id, :string, null: false
+ # impersonation_session_logs
+ change_column :impersonation_session_logs, :id, :string, null: false
+ change_column :impersonation_session_logs, :impersonation_session_id, :string, null: false
+ # impersonation_sessions
+ change_column :impersonation_sessions, :id, :string, null: false
+ change_column :impersonation_sessions, :impersonator_id, :string, null: false
+ change_column :impersonation_sessions, :impersonated_id, :string, null: false
+ # import_mappings
+ change_column :import_mappings, :id, :string, null: false
+ change_column :import_mappings, :import_id, :string, null: false
+ change_column :import_mappings, :mappable_id, :string
+ # import_rows
+ change_column :import_rows, :id, :string, null: false
+ change_column :import_rows, :import_id, :string, null: false
+ # imports
+ change_column :imports, :id, :string, null: false
+ change_column :imports, :family_id, :string, null: false
+ change_column :imports, :account_id, :string
+ # investments
+ change_column :investments, :id, :string, null: false
+ # invitations
+ change_column :invitations, :id, :string, null: false
+ change_column :invitations, :family_id, :string, null: false
+ change_column :invitations, :inviter_id, :string, null: false
+ # invite_codes
+ change_column :invite_codes, :id, :string, null: false
+ # loans
+ change_column :loans, :id, :string, null: false
+ # merchants
+ change_column :merchants, :id, :string, null: false
+ change_column :merchants, :family_id, :string
+ # messages
+ change_column :messages, :id, :string, null: false
+ change_column :messages, :chat_id, :string, null: false
+ # mobile_devices
+ change_column :mobile_devices, :id, :string, null: false
+ change_column :mobile_devices, :user_id, :string, null: false
+ # oauth_access_grants
+ # change_column :oauth_access_grants, :id, :string, null: false
+ # oauth_access_tokens
+ # change_column :oauth_access_tokens, :id, :string, null: false
+ # oauth_applications
+ # change_column :oauth_applications, :id, :string, null: false
+ change_column :oauth_applications, :owner_id, :string
+ # other_assets
+ change_column :other_assets, :id, :string, null: false
+ # other_liabilities
+ change_column :other_liabilities, :id, :string, null: false
+ # plaid_accounts
+ change_column :plaid_accounts, :id, :string, null: false
+ change_column :plaid_accounts, :plaid_item_id, :string, null: false
+ # plaid_items
+ change_column :plaid_items, :id, :string, null: false
+ change_column :plaid_items, :family_id, :string, null: false
+ # properties
+ change_column :properties, :id, :string, null: false
+
+ # rejected_transfers
+ change_column :rejected_transfers, :id, :string, null: false
+ change_column :rejected_transfers, :inflow_transaction_id, :string, null: false
+ change_column :rejected_transfers, :outflow_transaction_id, :string, null: false
+ # rule_actions
+ change_column :rule_actions, :id, :string, null: false
+ change_column :rule_actions, :rule_id, :string, null: false
+ # rule_conditions
+ change_column :rule_conditions, :id, :string, null: false
+ change_column :rule_conditions, :rule_id, :string
+ change_column :rule_conditions, :parent_id, :string
+ # rules
+ change_column :rules, :id, :string, null: false
+ change_column :rules, :family_id, :string, null: false
+ # securities
+ change_column :securities, :id, :string, null: false
+ # security_prices
+ change_column :security_prices, :id, :string, null: false
+ change_column :security_prices, :security_id, :string
+ # sessions
+ change_column :sessions, :id, :string, null: false
+ change_column :sessions, :user_id, :string, null: false
+ change_column :sessions, :active_impersonator_session_id, :string
+ # settings
+ # change_column :settings, :id, :string, null: false
+ # simplefin_accounts
+ change_column :simplefin_accounts, :id, :string, null: false
+ change_column :simplefin_accounts, :simplefin_item_id, :string, null: false
+ # simplefin_items
+ change_column :simplefin_items, :id, :string, null: false
+ change_column :simplefin_items, :family_id, :string, null: false
+ # subscriptions
+ change_column :subscriptions, :id, :string, null: false
+ change_column :subscriptions, :family_id, :string, null: false
+ # syncs
+ change_column :syncs, :id, :string, null: false
+ change_column :syncs, :syncable_id, :string, null: false
+ change_column :syncs, :parent_id, :string
+ # taggings
+ change_column :taggings, :id, :string, null: false
+ change_column :taggings, :tag_id, :string, null: false
+ change_column :taggings, :taggable_id, :string
+ # tags
+ change_column :tags, :id, :string, null: false
+ change_column :tags, :family_id, :string, null: false
+ # tool_calls
+ change_column :tool_calls, :id, :string, null: false
+ change_column :tool_calls, :message_id, :string, null: false
+ # trades
+ change_column :trades, :id, :string, null: false
+ change_column :trades, :security_id, :string, null: false
+ # transactions
+ change_column :transactions, :id, :string, null: false
+ change_column :transactions, :category_id, :string
+ change_column :transactions, :merchant_id, :string
+ # transfers
+ change_column :transfers, :id, :string, null: false
+ change_column :transfers, :inflow_transaction_id, :string, null: false
+ change_column :transfers, :outflow_transaction_id, :string, null: false
+ # users
+ change_column :users, :id, :string, null: false
+ change_column :users, :family_id, :string, null: false
+ change_column :users, :last_viewed_chat_id, :string
+
+ # valuations
+ change_column :valuations, :id, :string, null: false
+ # vehicles
+ change_column :vehicles, :id, :string, null: false
+
+ add_all_foreign_keys
+ # raise "Not implemented yet, WHAT"
+ end
+
+ def remove_all_foreign_keys
+ remove_foreign_key :accounts, :families
+ remove_foreign_key :accounts, :imports
+ remove_foreign_key :accounts, :plaid_accounts
+ remove_foreign_key :accounts, :simplefin_accounts
+ remove_foreign_key :active_storage_attachments, :active_storage_blobs
+ remove_foreign_key :active_storage_variant_records, :active_storage_blobs
+ remove_foreign_key :api_keys, :users
+ remove_foreign_key :balances, :accounts
+ remove_foreign_key :budget_categories, :budgets
+ remove_foreign_key :budget_categories, :categories
+ remove_foreign_key :budgets, :families
+ remove_foreign_key :categories, :families
+ remove_foreign_key :chats, :users
+ remove_foreign_key :entries, :accounts
+ remove_foreign_key :entries, :imports
+ remove_foreign_key :family_exports, :families
+ remove_foreign_key :holdings, :accounts
+ remove_foreign_key :holdings, :securities
+ remove_foreign_key :impersonation_session_logs, :impersonation_sessions
+ remove_foreign_key :impersonation_sessions, :users, column: :impersonated_id
+ remove_foreign_key :impersonation_sessions, :users, column: :impersonator_id
+ remove_foreign_key :import_rows, :imports
+ remove_foreign_key :imports, :families
+ remove_foreign_key :invitations, :families
+ remove_foreign_key :invitations, :users, column: :inviter_id
+ remove_foreign_key :merchants, :families
+ remove_foreign_key :messages, :chats
+ remove_foreign_key :mobile_devices, :users
+ remove_foreign_key :oauth_access_grants, :oauth_applications, column: :application_id
+ remove_foreign_key :oauth_access_tokens, :oauth_applications, column: :application_id
+ remove_foreign_key :plaid_accounts, :plaid_items
+ remove_foreign_key :plaid_items, :families
+ remove_foreign_key :rejected_transfers, :transactions, column: :inflow_transaction_id
+ remove_foreign_key :rejected_transfers, :transactions, column: :outflow_transaction_id
+ remove_foreign_key :rule_actions, :rules
+ remove_foreign_key :rule_conditions, :rule_conditions, column: :parent_id
+ remove_foreign_key :rule_conditions, :rules
+ remove_foreign_key :rules, :families
+ remove_foreign_key :security_prices, :securities
+ remove_foreign_key :sessions, :impersonation_sessions, column: :active_impersonator_session_id
+ remove_foreign_key :sessions, :users
+ remove_foreign_key :simplefin_accounts, :simplefin_items
+ remove_foreign_key :simplefin_items, :families
+ remove_foreign_key :subscriptions, :families
+ remove_foreign_key :syncs, :syncs, column: :parent_id
+ remove_foreign_key :taggings, :tags
+ remove_foreign_key :tags, :families
+ remove_foreign_key :tool_calls, :messages
+ remove_foreign_key :trades, :securities
+ remove_foreign_key :transactions, :categories
+ remove_foreign_key :transactions, :merchants
+ remove_foreign_key :transfers, :transactions, column: :inflow_transaction_id
+ remove_foreign_key :transfers, :transactions, column: :outflow_transaction_id
+ remove_foreign_key :users, :chats, column: :last_viewed_chat_id
+ remove_foreign_key :users, :families
+ end
+
+ def add_all_foreign_keys
+ add_foreign_key :accounts, :families
+ add_foreign_key :accounts, :imports
+ add_foreign_key :accounts, :plaid_accounts
+ add_foreign_key :accounts, :simplefin_accounts
+ add_foreign_key :active_storage_attachments, :active_storage_blobs, column: :blob_id
+ add_foreign_key :active_storage_variant_records, :active_storage_blobs, column: :blob_id
+ add_foreign_key :api_keys, :users
+ add_foreign_key :balances, :accounts, on_delete: :cascade
+ add_foreign_key :budget_categories, :budgets
+ add_foreign_key :budget_categories, :categories
+ add_foreign_key :budgets, :families
+ add_foreign_key :categories, :families
+ add_foreign_key :chats, :users
+ add_foreign_key :entries, :accounts
+ add_foreign_key :entries, :imports
+ add_foreign_key :family_exports, :families
+ add_foreign_key :holdings, :accounts
+ add_foreign_key :holdings, :securities
+ add_foreign_key :impersonation_session_logs, :impersonation_sessions
+ add_foreign_key :impersonation_sessions, :users, column: :impersonated_id
+ add_foreign_key :impersonation_sessions, :users, column: :impersonator_id
+ add_foreign_key :import_rows, :imports
+ add_foreign_key :imports, :families
+ add_foreign_key :invitations, :families
+ add_foreign_key :invitations, :users, column: :inviter_id
+ add_foreign_key :merchants, :families
+ add_foreign_key :messages, :chats
+ add_foreign_key :mobile_devices, :users
+ add_foreign_key :oauth_access_grants, :oauth_applications, column: :application_id
+ add_foreign_key :oauth_access_tokens, :oauth_applications, column: :application_id
+ add_foreign_key :plaid_accounts, :plaid_items
+ add_foreign_key :plaid_items, :families
+ add_foreign_key :rejected_transfers, :transactions, column: :inflow_transaction_id
+ add_foreign_key :rejected_transfers, :transactions, column: :outflow_transaction_id
+ add_foreign_key :rule_actions, :rules
+ add_foreign_key :rule_conditions, :rule_conditions, column: :parent_id
+ add_foreign_key :rule_conditions, :rules
+ add_foreign_key :rules, :families
+ add_foreign_key :security_prices, :securities
+ add_foreign_key :sessions, :impersonation_sessions, column: :active_impersonator_session_id
+ add_foreign_key :sessions, :users
+ add_foreign_key :simplefin_accounts, :simplefin_items
+ add_foreign_key :simplefin_items, :families
+ add_foreign_key :subscriptions, :families
+ add_foreign_key :syncs, :syncs, column: :parent_id
+ add_foreign_key :taggings, :tags
+ add_foreign_key :tags, :families
+ add_foreign_key :tool_calls, :messages
+ add_foreign_key :trades, :securities
+ add_foreign_key :transactions, :categories, on_delete: :nullify
+ add_foreign_key :transactions, :merchants
+ add_foreign_key :transfers, :transactions, column: :inflow_transaction_id, on_delete: :cascade
+ add_foreign_key :transfers, :transactions, column: :outflow_transaction_id, on_delete: :cascade
+ add_foreign_key :users, :chats, column: :last_viewed_chat_id
+ add_foreign_key :users, :families
+ end
+end
diff --git a/db/migrate/20250924142630_change_json_b_to_json.rb b/db/migrate/20250924142630_change_json_b_to_json.rb
new file mode 100644
index 00000000000..0a658b2a646
--- /dev/null
+++ b/db/migrate/20250924142630_change_json_b_to_json.rb
@@ -0,0 +1,54 @@
+class ChangeJsonBToJson < ActiveRecord::Migration[7.2]
+ def change
+ change_column :accounts, :locked_attributes, :json
+ change_column :chats, :error, :json
+ change_column :credit_cards, :locked_attributes, :json
+ change_column :cryptos, :locked_attributes, :json
+
+ change_column :data_enrichments, :value, :json
+ change_column :data_enrichments, :metadata, :json
+
+ change_column :depositories, :locked_attributes, :json
+
+ change_column :entries, :locked_attributes, :json
+
+ change_column :imports, :column_mappings, :json
+
+ change_column :investments, :locked_attributes, :json
+
+ change_column :loans, :locked_attributes, :json
+
+ change_column :other_assets, :locked_attributes, :json
+
+ change_column :other_liabilities, :locked_attributes, :json
+
+ change_column :plaid_accounts, :raw_payload, :json
+ change_column :plaid_accounts, :raw_transactions_payload, :json
+ change_column :plaid_accounts, :raw_investments_payload, :json
+ change_column :plaid_accounts, :raw_liabilities_payload, :json
+
+ change_column :plaid_items, :raw_payload, :json
+ change_column :plaid_items, :raw_institution_payload, :json
+
+ change_column :properties, :locked_attributes, :json
+
+ change_column :sessions, :prev_transaction_page_params, :json
+ change_column :sessions, :data, :json
+
+ change_column :simplefin_accounts, :raw_payload, :json
+ change_column :simplefin_accounts, :raw_transactions_payload, :json
+ change_column :simplefin_accounts, :extra, :json
+ change_column :simplefin_accounts, :org_data, :json
+
+ change_column :simplefin_items, :raw_payload, :json
+ change_column :simplefin_items, :raw_institution_payload, :json
+
+ change_column :syncs, :data, :json
+
+ change_column :tool_calls, :function_arguments, :json
+ change_column :tool_calls, :function_result, :json
+
+ change_column :trades, :locked_attributes, :json
+ change_column :transactions, :locked_attributes, :json
+ end
+end
diff --git a/db/migrate/20250924190901_convert_accounts_classification_to_regular_column.rb b/db/migrate/20250924190901_convert_accounts_classification_to_regular_column.rb
new file mode 100644
index 00000000000..bdf38669fb3
--- /dev/null
+++ b/db/migrate/20250924190901_convert_accounts_classification_to_regular_column.rb
@@ -0,0 +1,19 @@
+class ConvertAccountsClassificationToRegularColumn < ActiveRecord::Migration[7.0]
+ def change
+ # Remove the virtual column
+ remove_column :accounts, :classification, :string
+
+ # Add a regular string column
+ add_column :accounts, :classification, :string
+
+ # Populate the classification column based on accountable_type
+ execute <<-SQL
+ UPDATE accounts#{' '}
+ SET classification = CASE
+ WHEN accountable_type IN ('Loan', 'CreditCard', 'OtherLiability')
+ THEN 'liability'
+ ELSE 'asset'
+ END
+ SQL
+ end
+end
diff --git a/db/migrate/20250924191112_convert_account_balances_virtual_columns_to_regular_columns.rb b/db/migrate/20250924191112_convert_account_balances_virtual_columns_to_regular_columns.rb
new file mode 100644
index 00000000000..c02d4811e53
--- /dev/null
+++ b/db/migrate/20250924191112_convert_account_balances_virtual_columns_to_regular_columns.rb
@@ -0,0 +1,25 @@
+class ConvertAccountBalancesVirtualColumnsToRegularColumns < ActiveRecord::Migration[7.0]
+ def change
+ # Remove all virtual columns
+ remove_column :balances, :start_balance, :decimal
+ remove_column :balances, :end_cash_balance, :decimal
+ remove_column :balances, :end_non_cash_balance, :decimal
+ remove_column :balances, :end_balance, :decimal
+
+ # Add regular decimal columns
+ add_column :balances, :start_balance, :decimal, precision: 19, scale: 4
+ add_column :balances, :end_cash_balance, :decimal, precision: 19, scale: 4
+ add_column :balances, :end_non_cash_balance, :decimal, precision: 19, scale: 4
+ add_column :balances, :end_balance, :decimal, precision: 19, scale: 4
+
+ # Populate the columns based on the original virtual column logic
+ execute <<-SQL
+ UPDATE balances#{' '}
+ SET#{' '}
+ start_balance = (start_cash_balance + start_non_cash_balance),
+ end_cash_balance = ((start_cash_balance + ((cash_inflows - cash_outflows) * (flows_factor)::numeric)) + cash_adjustments),
+ end_non_cash_balance = (((start_non_cash_balance + ((non_cash_inflows - non_cash_outflows) * (flows_factor)::numeric)) + net_market_flows) + non_cash_adjustments),
+ end_balance = (((start_cash_balance + ((cash_inflows - cash_outflows) * (flows_factor)::numeric)) + cash_adjustments) + (((start_non_cash_balance + ((non_cash_inflows - non_cash_outflows) * (flows_factor)::numeric)) + net_market_flows) + non_cash_adjustments))
+ SQL
+ end
+end
diff --git a/db/migrate/20250924194505_remove_functional_and_partial_indexes.rb b/db/migrate/20250924194505_remove_functional_and_partial_indexes.rb
new file mode 100644
index 00000000000..0eb4753956a
--- /dev/null
+++ b/db/migrate/20250924194505_remove_functional_and_partial_indexes.rb
@@ -0,0 +1,25 @@
+class RemoveFunctionalAndPartialIndexes < ActiveRecord::Migration[7.0]
+ def change
+ # Remove functional index on entries.name
+ remove_index :entries, name: "index_entries_on_lower_name"
+
+ # Remove partial indexes on merchants
+ remove_index :merchants, name: "index_merchants_on_family_id_and_name"
+ remove_index :merchants, name: "index_merchants_on_source_and_name"
+
+ # Remove partial index on users.otp_secret
+ remove_index :users, name: "index_users_on_otp_secret"
+
+ # Add regular indexes to replace them
+
+ # Regular index on entries.name (for case-insensitive searches, handle in application)
+ add_index :entries, :name, name: "index_entries_on_name"
+
+ # Regular indexes on merchants (remove unique constraint since we can't enforce it per type)
+ add_index :merchants, [ :family_id, :name ], name: "index_merchants_on_family_id_and_name"
+ add_index :merchants, [ :source, :name ], name: "index_merchants_on_source_and_name"
+
+ # Regular index on users.otp_secret (remove unique constraint since we can't enforce it for non-null only)
+ add_index :users, :otp_secret, name: "index_users_on_otp_secret"
+ end
+end
diff --git a/db/migrate/20250924215505_convert_billed_products_array_to_json.rb b/db/migrate/20250924215505_convert_billed_products_array_to_json.rb
new file mode 100644
index 00000000000..2ff85d1909e
--- /dev/null
+++ b/db/migrate/20250924215505_convert_billed_products_array_to_json.rb
@@ -0,0 +1,30 @@
+class ConvertBilledProductsArrayToJson < ActiveRecord::Migration[7.0]
+ def change
+ # Remove PostgreSQL array columns
+ remove_column :plaid_items, :available_products, :string, array: true
+ remove_column :plaid_items, :billed_products, :string, array: true
+ remove_column :users, :otp_backup_codes, :string, array: true
+ remove_column :users, :goals, :text, array: true
+
+ # Add JSON columns with default empty array
+ add_column :plaid_items, :available_products, :json, default: []
+ add_column :plaid_items, :billed_products, :json, default: []
+ add_column :users, :otp_backup_codes, :json, default: []
+ add_column :users, :goals, :json, default: []
+
+ # Convert JSONB to JSON for database compatibility
+ remove_column :valuations, :locked_attributes, :jsonb
+ remove_column :vehicles, :locked_attributes, :jsonb
+
+ add_column :valuations, :locked_attributes, :json, default: {}
+ add_column :vehicles, :locked_attributes, :json, default: {}
+
+ # Remove PostgreSQL-specific functional index
+ remove_index :securities, name: "index_securities_on_ticker_and_exchange_operating_mic_unique"
+
+ # Add regular index instead
+ add_index :securities, [ :ticker, :exchange_operating_mic ],
+ name: "index_securities_on_ticker_and_exchange_operating_mic_unique",
+ unique: true
+ end
+end
diff --git a/db/migrate/20250927190818_remove_uuid_default_value.rb b/db/migrate/20250927190818_remove_uuid_default_value.rb
new file mode 100644
index 00000000000..75963b78447
--- /dev/null
+++ b/db/migrate/20250927190818_remove_uuid_default_value.rb
@@ -0,0 +1,88 @@
+
+class RemoveUuidDefaultValue < ActiveRecord::Migration[7.2]
+ def up
+ # Remove PostgreSQL-specific UUID defaults for SQLite compatibility
+ # SQLite doesn't support gen_random_uuid(), so we'll handle UUIDs in Ruby
+
+ # List of tables that need UUID default removal
+ tables_with_uuid_defaults = [
+ 'accounts',
+ 'active_storage_attachments',
+ 'active_storage_blobs',
+ 'active_storage_variant_records',
+ 'addresses',
+ 'api_keys',
+ 'balances',
+ 'budget_categories',
+ 'budgets',
+ 'categories',
+ 'chats',
+ 'credit_cards',
+ 'cryptos',
+ 'data_enrichments',
+ 'depositories',
+ 'entries',
+ 'exchange_rates',
+ 'families',
+ 'family_exports',
+ 'holdings',
+ 'impersonation_session_logs',
+ 'impersonation_sessions',
+ 'import_mappings',
+ 'import_rows',
+ 'imports',
+ 'investments',
+ 'invitations',
+ 'invite_codes',
+ 'loans',
+ 'merchants',
+ 'messages',
+ 'mobile_devices',
+ # 'oauth_access_grants',
+ # 'oauth_access_tokens',
+ # 'oauth_applications',
+ 'other_assets',
+ 'other_liabilities',
+ 'plaid_accounts',
+ 'plaid_items',
+ 'properties',
+ 'rejected_transfers',
+ 'rule_actions',
+ 'rule_conditions',
+ 'rules',
+ 'securities',
+ 'security_prices',
+ 'sessions',
+ # 'settings',
+ 'simplefin_accounts',
+ 'simplefin_items',
+ 'subscriptions',
+ 'syncs',
+ 'taggings',
+ 'tags',
+ 'tool_calls',
+ 'trades',
+ 'transactions',
+ 'transfers',
+ 'users',
+ 'valuations',
+ 'vehicles'
+ ]
+
+ tables_with_uuid_defaults.each do |table_name|
+ if table_exists?(table_name)
+ # Remove the default UUID generation for SQLite compatibility
+ change_column_default table_name, :id, nil
+ puts "Removed UUID default from #{table_name}"
+ else
+ raise ActiveRecord::MigrationError, "Table #{table_name} does not exist"
+ end
+ end
+ end
+
+ def down
+ # This migration is not reversible as we can't restore PostgreSQL-specific defaults
+ # in a SQLite environment
+ # raise ActiveRecord::IrreversibleMigration, "Cannot restore PostgreSQL UUID defaults in SQLite"
+ end
+end
diff --git a/db/migrate/20250928213010_add_uuid_defaults_to_active_storage_tables.rb b/db/migrate/20250928213010_add_uuid_defaults_to_active_storage_tables.rb
new file mode 100644
index 00000000000..d80a63c1272
--- /dev/null
+++ b/db/migrate/20250928213010_add_uuid_defaults_to_active_storage_tables.rb
@@ -0,0 +1,8 @@
+class AddUuidDefaultsToActiveStorageTables < ActiveRecord::Migration[7.2]
+ def change
+ # Add UUID default values for Active Storage tables primary keys
+ change_column_default :active_storage_attachments, :id, -> { "gen_random_uuid()" }
+ change_column_default :active_storage_blobs, :id, -> { "gen_random_uuid()" }
+ change_column_default :active_storage_variant_records, :id, -> { "gen_random_uuid()" }
+ end
+end
diff --git a/db/migrate/20250928220425_add_uuid_default_to_categories_table.rb b/db/migrate/20250928220425_add_uuid_default_to_categories_table.rb
new file mode 100644
index 00000000000..592b3fe6a25
--- /dev/null
+++ b/db/migrate/20250928220425_add_uuid_default_to_categories_table.rb
@@ -0,0 +1,6 @@
+class AddUuidDefaultToCategoriesTable < ActiveRecord::Migration[7.2]
+ def change
+ # Add UUID default value for categories table primary key
+ change_column_default :categories, :id, -> { "gen_random_uuid()" }
+ end
+end
diff --git a/db/migrate/20250929215928_add_uuid_defaults_to_all_tables.rb b/db/migrate/20250929215928_add_uuid_defaults_to_all_tables.rb
new file mode 100644
index 00000000000..1abe72d610a
--- /dev/null
+++ b/db/migrate/20250929215928_add_uuid_defaults_to_all_tables.rb
@@ -0,0 +1,77 @@
+class AddUuidDefaultsToAllTables < ActiveRecord::Migration[7.2]
+ def change
+ # Remove PostgreSQL-specific UUID defaults for SQLite compatibility
+ # SQLite doesn't support gen_random_uuid(), so we'll handle UUIDs in Ruby
+
+ # List of tables that need UUID default removal
+ tables_with_uuid_defaults = [
+ 'accounts',
+ 'addresses',
+ 'api_keys',
+ 'balances',
+ 'budget_categories',
+ 'budgets',
+ 'chats',
+ 'credit_cards',
+ 'cryptos',
+ 'data_enrichments',
+ 'depositories',
+ 'entries',
+ 'exchange_rates',
+ 'families',
+ 'family_exports',
+ 'holdings',
+ 'impersonation_session_logs',
+ 'impersonation_sessions',
+ 'import_mappings',
+ 'import_rows',
+ 'imports',
+ 'investments',
+ 'invitations',
+ 'invite_codes',
+ 'loans',
+ 'merchants',
+ 'messages',
+ 'mobile_devices',
+ # 'oauth_access_grants',
+ # 'oauth_access_tokens',
+ # 'oauth_applications',
+ 'other_assets',
+ 'other_liabilities',
+ 'plaid_accounts',
+ 'plaid_items',
+ 'properties',
+ 'rejected_transfers',
+ 'rule_actions',
+ 'rule_conditions',
+ 'rules',
+ 'securities',
+ 'security_prices',
+ 'sessions',
+ # 'settings',
+ 'simplefin_accounts',
+ 'simplefin_items',
+ 'subscriptions',
+ 'syncs',
+ 'taggings',
+ 'tags',
+ 'tool_calls',
+ 'trades',
+ 'transactions',
+ 'transfers',
+ 'users',
+ 'valuations',
+ 'vehicles'
+ ]
+
+ tables_with_uuid_defaults.each do |table_name|
+ if table_exists?(table_name)
+ # Remove the default UUID generation for SQLite compatibility
+ change_column_default table_name, :id, nil
+ puts "Removed UUID default from #{table_name}"
+ else
+ raise ActiveRecord::MigrationError, "Table #{table_name} does not exist"
+ end
+ end
+ end
+end
diff --git a/db/migrate/20250930191642_change_default_id_to_ulid.rb b/db/migrate/20250930191642_change_default_id_to_ulid.rb
new file mode 100644
index 00000000000..20d7a0da3aa
--- /dev/null
+++ b/db/migrate/20250930191642_change_default_id_to_ulid.rb
@@ -0,0 +1,17 @@
+class ChangeDefaultIdToUlid < ActiveRecord::Migration[7.2]
+ def up
+ # Change default value from gen_random_uuid() to ulid() for tables that use it
+ change_column_default :active_storage_attachments, :id, -> { "ulid()" }
+ change_column_default :active_storage_blobs, :id, -> { "ulid()" }
+ change_column_default :active_storage_variant_records, :id, -> { "ulid()" }
+ change_column_default :categories, :id, -> { "ulid()" }
+ end
+
+ def down
+ # Revert back to gen_random_uuid() if needed
+ change_column_default :active_storage_attachments, :id, -> { "gen_random_uuid()" }
+ change_column_default :active_storage_blobs, :id, -> { "gen_random_uuid()" }
+ change_column_default :active_storage_variant_records, :id, -> { "gen_random_uuid()" }
+ change_column_default :categories, :id, -> { "gen_random_uuid()" }
+ end
+end
diff --git a/db/migrate/20251003011536_add_service_name_to_active_storage_blobs.active_storage.rb b/db/migrate/20251003011536_add_service_name_to_active_storage_blobs.active_storage.rb
new file mode 100644
index 00000000000..a15c6ce8e58
--- /dev/null
+++ b/db/migrate/20251003011536_add_service_name_to_active_storage_blobs.active_storage.rb
@@ -0,0 +1,22 @@
+# This migration comes from active_storage (originally 20190112182829)
+class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0]
+ def up
+ return unless table_exists?(:active_storage_blobs)
+
+ unless column_exists?(:active_storage_blobs, :service_name)
+ add_column :active_storage_blobs, :service_name, :string
+
+ if configured_service = ActiveStorage::Blob.service.name
+ ActiveStorage::Blob.unscoped.update_all(service_name: configured_service)
+ end
+
+ change_column :active_storage_blobs, :service_name, :string, null: false
+ end
+ end
+
+ def down
+ return unless table_exists?(:active_storage_blobs)
+
+ remove_column :active_storage_blobs, :service_name
+ end
+end
diff --git a/db/migrate/20251003011537_create_active_storage_variant_records.active_storage.rb b/db/migrate/20251003011537_create_active_storage_variant_records.active_storage.rb
new file mode 100644
index 00000000000..4806b10e8e6
--- /dev/null
+++ b/db/migrate/20251003011537_create_active_storage_variant_records.active_storage.rb
@@ -0,0 +1,27 @@
+# This migration comes from active_storage (originally 20191206030411)
+class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
+ def change
+ return unless table_exists?(:active_storage_blobs)
+
+ # Use Active Record's configured type for primary key
+ create_table :active_storage_variant_records, id: primary_key_type, if_not_exists: true do |t|
+ t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type
+ t.string :variation_digest, null: false
+
+ t.index %i[blob_id variation_digest], name: "index_active_storage_variant_records_uniqueness", unique: true
+ t.foreign_key :active_storage_blobs, column: :blob_id
+ end
+ end
+
+ private
+ def primary_key_type
+ config = Rails.configuration.generators
+ config.options[config.orm][:primary_key_type] || :primary_key
+ end
+
+ def blobs_primary_key_type
+ pkey_name = connection.primary_key(:active_storage_blobs)
+ pkey_column = connection.columns(:active_storage_blobs).find { |c| c.name == pkey_name }
+ pkey_column.bigint? ? :bigint : pkey_column.type
+ end
+end
diff --git a/db/migrate/20251003011538_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb b/db/migrate/20251003011538_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb
new file mode 100644
index 00000000000..93c8b85ade5
--- /dev/null
+++ b/db/migrate/20251003011538_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb
@@ -0,0 +1,8 @@
+# This migration comes from active_storage (originally 20211119233751)
+class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0]
+ def change
+ return unless table_exists?(:active_storage_blobs)
+
+ change_column_null(:active_storage_blobs, :checksum, true)
+ end
+end
diff --git a/db/migrate/20251008015408_update_id_defaults_to_gen_random_uuid.rb b/db/migrate/20251008015408_update_id_defaults_to_gen_random_uuid.rb
new file mode 100644
index 00000000000..312679b9d12
--- /dev/null
+++ b/db/migrate/20251008015408_update_id_defaults_to_gen_random_uuid.rb
@@ -0,0 +1,27 @@
+class UpdateIdDefaultsToGenRandomUuid < ActiveRecord::Migration[8.0]
+ def change
+ # List of all tables with string ID columns that need gen_random_uuid() default
+ tables_with_string_ids = [
+ 'accounts', 'addresses', 'api_keys', 'balances', 'budget_categories', 'budgets',
+ 'chats', 'credit_cards', 'cryptos', 'data_enrichments', 'depositories', 'entries',
+ 'exchange_rates', 'families', 'family_exports', 'holdings', 'impersonation_session_logs',
+ 'impersonation_sessions', 'import_mappings', 'import_rows', 'imports', 'investments',
+ 'invitations', 'invite_codes', 'loans', 'merchants', 'messages', 'mobile_devices',
+ 'other_assets', 'other_liabilities', 'plaid_accounts', 'plaid_items', 'properties',
+ 'rejected_transfers', 'rule_actions', 'rule_conditions', 'rules', 'securities',
+ 'security_prices', 'sessions', 'simplefin_accounts', 'simplefin_items', 'subscriptions',
+ 'syncs', 'taggings', 'tags', 'tool_calls', 'trades', 'transactions', 'transfers',
+ 'users', 'valuations', 'vehicles',
+ 'active_storage_attachments', 'active_storage_blobs', 'active_storage_variant_records',
+ 'categories'
+ ]
+
+ tables_with_string_ids.each do |table_name|
+ # Check if table exists and has an id column
+ if table_exists?(table_name) && column_exists?(table_name, :id)
+ # Update the id column to use gen_random_uuid() as default
+ change_column_default table_name, :id, -> { "gen_random_uuid()" }
+ end
+ end
+ end
+end
diff --git a/db/queue_schema.rb b/db/queue_schema.rb
new file mode 100644
index 00000000000..f56798cab3a
--- /dev/null
+++ b/db/queue_schema.rb
@@ -0,0 +1,141 @@
+# This file is auto-generated from the current state of the database. Instead
+# of editing this file, please use the migrations feature of Active Record to
+# incrementally modify your database, and then regenerate this schema definition.
+#
+# This file is the source Rails uses to define your schema when running `bin/rails
+# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
+# be faster and is potentially less error prone than running all of your
+# migrations from scratch. Old migrations may fail to apply correctly if those
+# migrations use external dependencies or application code.
+#
+# It's strongly recommended that you check this file into your version control system.
+
+ActiveRecord::Schema[8.1].define(version: 1) do
+ create_table "solid_queue_blocked_executions", force: :cascade do |t|
+ t.string "concurrency_key", null: false
+ t.datetime "created_at", null: false
+ t.datetime "expires_at", null: false
+ t.bigint "job_id", null: false
+ t.integer "priority", default: 0, null: false
+ t.string "queue_name", null: false
+ t.index ["concurrency_key", "priority", "job_id"], name: "index_solid_queue_blocked_executions_for_release"
+ t.index ["expires_at", "concurrency_key"], name: "index_solid_queue_blocked_executions_for_maintenance"
+ t.index ["job_id"], name: "index_solid_queue_blocked_executions_on_job_id", unique: true
+ end
+
+ create_table "solid_queue_claimed_executions", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.bigint "job_id", null: false
+ t.bigint "process_id"
+ t.index ["job_id"], name: "index_solid_queue_claimed_executions_on_job_id", unique: true
+ t.index ["process_id", "job_id"], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id"
+ end
+
+ create_table "solid_queue_failed_executions", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.text "error"
+ t.bigint "job_id", null: false
+ t.index ["job_id"], name: "index_solid_queue_failed_executions_on_job_id", unique: true
+ end
+
+ create_table "solid_queue_jobs", force: :cascade do |t|
+ t.string "active_job_id"
+ t.text "arguments"
+ t.string "class_name", null: false
+ t.string "concurrency_key"
+ t.datetime "created_at", null: false
+ t.datetime "finished_at"
+ t.integer "priority", default: 0, null: false
+ t.string "queue_name", null: false
+ t.datetime "scheduled_at"
+ t.datetime "updated_at", null: false
+ t.index ["active_job_id"], name: "index_solid_queue_jobs_on_active_job_id"
+ t.index ["class_name"], name: "index_solid_queue_jobs_on_class_name"
+ t.index ["finished_at"], name: "index_solid_queue_jobs_on_finished_at"
+ t.index ["queue_name", "finished_at"], name: "index_solid_queue_jobs_for_filtering"
+ t.index ["scheduled_at", "finished_at"], name: "index_solid_queue_jobs_for_alerting"
+ end
+
+ create_table "solid_queue_pauses", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.string "queue_name", null: false
+ t.index ["queue_name"], name: "index_solid_queue_pauses_on_queue_name", unique: true
+ end
+
+ create_table "solid_queue_processes", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.string "hostname"
+ t.string "kind", null: false
+ t.datetime "last_heartbeat_at", null: false
+ t.text "metadata"
+ t.string "name", null: false
+ t.integer "pid", null: false
+ t.bigint "supervisor_id"
+ t.index ["last_heartbeat_at"], name: "index_solid_queue_processes_on_last_heartbeat_at"
+ t.index ["name", "supervisor_id"], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true
+ t.index ["supervisor_id"], name: "index_solid_queue_processes_on_supervisor_id"
+ end
+
+ create_table "solid_queue_ready_executions", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.bigint "job_id", null: false
+ t.integer "priority", default: 0, null: false
+ t.string "queue_name", null: false
+ t.index ["job_id"], name: "index_solid_queue_ready_executions_on_job_id", unique: true
+ t.index ["priority", "job_id"], name: "index_solid_queue_poll_all"
+ t.index ["queue_name", "priority", "job_id"], name: "index_solid_queue_poll_by_queue"
+ end
+
+ create_table "solid_queue_recurring_executions", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.bigint "job_id", null: false
+ t.datetime "run_at", null: false
+ t.string "task_key", null: false
+ t.index ["job_id"], name: "index_solid_queue_recurring_executions_on_job_id", unique: true
+ t.index ["task_key", "run_at"], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true
+ end
+
+ create_table "solid_queue_recurring_tasks", force: :cascade do |t|
+ t.text "arguments"
+ t.string "class_name"
+ t.string "command", limit: 2048
+ t.datetime "created_at", null: false
+ t.text "description"
+ t.string "key", null: false
+ t.integer "priority", default: 0
+ t.string "queue_name"
+ t.string "schedule", null: false
+ t.boolean "static", default: true, null: false
+ t.datetime "updated_at", null: false
+ t.index ["key"], name: "index_solid_queue_recurring_tasks_on_key", unique: true
+ t.index ["static"], name: "index_solid_queue_recurring_tasks_on_static"
+ end
+
+ create_table "solid_queue_scheduled_executions", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.bigint "job_id", null: false
+ t.integer "priority", default: 0, null: false
+ t.string "queue_name", null: false
+ t.datetime "scheduled_at", null: false
+ t.index ["job_id"], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true
+ t.index ["scheduled_at", "priority", "job_id"], name: "index_solid_queue_dispatch_all"
+ end
+
+ create_table "solid_queue_semaphores", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.datetime "expires_at", null: false
+ t.string "key", null: false
+ t.datetime "updated_at", null: false
+ t.integer "value", default: 1, null: false
+ t.index ["expires_at"], name: "index_solid_queue_semaphores_on_expires_at"
+ t.index ["key", "value"], name: "index_solid_queue_semaphores_on_key_and_value"
+ t.index ["key"], name: "index_solid_queue_semaphores_on_key", unique: true
+ end
+
+ add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
+ add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
+ add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
+ add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
+ add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
+ add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
+end
diff --git a/db/schema.rb b/db/schema.rb
index 1c11eea005e..1437cb2bac8 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,60 +10,24 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.2].define(version: 2026_05_03_180000) do
- # These are extensions that must be enabled in order to support this database
- enable_extension "pgcrypto"
- enable_extension "plpgsql"
-
- # Custom types defined in this database.
- # Note that some types may not work with other database engines. Be careful if changing database.
- create_enum "account_status", ["ok", "syncing", "error"]
-
- create_table "account_providers", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "account_id", null: false
- t.string "provider_type", null: false
- t.uuid "provider_id", null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.index ["account_id", "provider_type"], name: "index_account_providers_on_account_and_provider_type", unique: true
- t.index ["provider_type", "provider_id"], name: "index_account_providers_on_provider_type_and_provider_id", unique: true
- end
-
- create_table "account_shares", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "account_id", null: false
- t.uuid "user_id", null: false
- t.string "permission", default: "read_only", null: false
- t.boolean "include_in_finances", default: true, null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.index ["account_id", "user_id"], name: "index_account_shares_on_account_id_and_user_id", unique: true
- t.index ["account_id"], name: "index_account_shares_on_account_id"
- t.index ["user_id", "include_in_finances"], name: "index_account_shares_on_user_id_and_include_in_finances"
- t.index ["user_id"], name: "index_account_shares_on_user_id"
- t.check_constraint "permission::text = ANY (ARRAY['full_control'::character varying::text, 'read_write'::character varying::text, 'read_only'::character varying::text])", name: "chk_account_shares_permission"
- end
-
- create_table "accounts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.string "subtype"
- t.uuid "family_id", null: false
- t.string "name"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ActiveRecord::Schema[8.1].define(version: 2025_10_08_015408) do
+ create_table "accounts", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.string "accountable_id"
t.string "accountable_type"
- t.uuid "accountable_id"
t.decimal "balance", precision: 19, scale: 4
- t.string "currency"
- t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY (ARRAY[('Loan'::character varying)::text, ('CreditCard'::character varying)::text, ('OtherLiability'::character varying)::text])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true
- t.uuid "import_id"
- t.uuid "plaid_account_id"
t.decimal "cash_balance", precision: 19, scale: 4, default: "0.0"
- t.jsonb "locked_attributes", default: {}
+ t.string "classification"
+ t.datetime "created_at", null: false
+ t.string "currency"
+ t.string "family_id", null: false
+ t.string "import_id"
+ t.json "locked_attributes", default: {}
+ t.string "name"
+ t.string "plaid_account_id"
+ t.string "simplefin_account_id"
t.string "status", default: "active"
- t.uuid "simplefin_account_id"
- t.string "institution_name"
- t.string "institution_domain"
- t.text "notes"
- t.uuid "owner_id"
+ t.string "subtype"
+ t.datetime "updated_at", null: false
t.index ["accountable_id", "accountable_type"], name: "index_accounts_on_accountable_id_and_accountable_type"
t.index ["accountable_type"], name: "index_accounts_on_accountable_type"
t.index ["currency"], name: "index_accounts_on_currency"
@@ -79,569 +43,254 @@
t.index ["status"], name: "index_accounts_on_status"
end
- create_table "active_storage_attachments", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ create_table "active_storage_attachments", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.string "blob_id", null: false
+ t.datetime "created_at", null: false
t.string "name", null: false
+ t.string "record_id", null: false
t.string "record_type", null: false
- t.uuid "record_id", null: false
- t.uuid "blob_id", null: false
- t.datetime "created_at", null: false
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
end
- create_table "active_storage_blobs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.string "key", null: false
- t.string "filename", null: false
- t.string "content_type"
- t.text "metadata"
- t.string "service_name", null: false
+ create_table "active_storage_blobs", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.bigint "byte_size", null: false
t.string "checksum"
+ t.string "content_type"
t.datetime "created_at", null: false
+ t.string "filename", null: false
+ t.string "key", null: false
+ t.text "metadata"
+ t.string "service_name", null: false
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
end
- create_table "active_storage_variant_records", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "blob_id", null: false
+ create_table "active_storage_variant_records", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.string "blob_id", null: false
t.string "variation_digest", null: false
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
end
- create_table "addresses", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ create_table "addresses", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.string "addressable_id"
t.string "addressable_type"
- t.uuid "addressable_id"
+ t.string "country"
+ t.string "county"
+ t.datetime "created_at", null: false
t.string "line1"
t.string "line2"
- t.string "county"
t.string "locality"
+ t.integer "postal_code"
t.string "region"
- t.string "country"
- t.string "postal_code"
- t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["addressable_type", "addressable_id"], name: "index_addresses_on_addressable"
end
- create_table "api_keys", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.string "name"
- t.uuid "user_id", null: false
- t.json "scopes"
- t.datetime "last_used_at"
- t.datetime "expires_at"
- t.datetime "revoked_at"
+ create_table "api_keys", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
t.string "display_key", null: false
+ t.datetime "expires_at"
+ t.datetime "last_used_at"
+ t.string "name"
+ t.datetime "revoked_at"
+ t.json "scopes"
t.string "source", default: "web"
+ t.datetime "updated_at", null: false
+ t.string "user_id", null: false
t.index ["display_key"], name: "index_api_keys_on_display_key", unique: true
t.index ["revoked_at"], name: "index_api_keys_on_revoked_at"
t.index ["user_id", "source"], name: "index_api_keys_on_user_id_and_source"
t.index ["user_id"], name: "index_api_keys_on_user_id"
end
- create_table "archived_exports", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.string "email", null: false
- t.string "family_name"
- t.string "download_token_digest", null: false
- t.datetime "expires_at", null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.index ["download_token_digest"], name: "index_archived_exports_on_download_token_digest", unique: true
- t.index ["expires_at"], name: "index_archived_exports_on_expires_at"
- end
-
- create_table "balances", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "account_id", null: false
- t.date "date", null: false
+ create_table "balances", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.string "account_id", null: false
t.decimal "balance", precision: 19, scale: 4, null: false
- t.string "currency", default: "USD", null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.decimal "cash_adjustments", precision: 19, scale: 4, default: "0.0", null: false
t.decimal "cash_balance", precision: 19, scale: 4, default: "0.0"
- t.decimal "start_cash_balance", precision: 19, scale: 4, default: "0.0", null: false
- t.decimal "start_non_cash_balance", precision: 19, scale: 4, default: "0.0", null: false
t.decimal "cash_inflows", precision: 19, scale: 4, default: "0.0", null: false
t.decimal "cash_outflows", precision: 19, scale: 4, default: "0.0", null: false
- t.decimal "non_cash_inflows", precision: 19, scale: 4, default: "0.0", null: false
- t.decimal "non_cash_outflows", precision: 19, scale: 4, default: "0.0", null: false
+ t.datetime "created_at", null: false
+ t.string "currency", default: "USD", null: false
+ t.date "date", null: false
+ t.decimal "end_balance", precision: 19, scale: 4
+ t.decimal "end_cash_balance", precision: 19, scale: 4
+ t.decimal "end_non_cash_balance", precision: 19, scale: 4
+ t.integer "flows_factor", default: 1, null: false
t.decimal "net_market_flows", precision: 19, scale: 4, default: "0.0", null: false
- t.decimal "cash_adjustments", precision: 19, scale: 4, default: "0.0", null: false
t.decimal "non_cash_adjustments", precision: 19, scale: 4, default: "0.0", null: false
- t.integer "flows_factor", default: 1, null: false
- t.virtual "start_balance", type: :decimal, precision: 19, scale: 4, as: "(start_cash_balance + start_non_cash_balance)", stored: true
- t.virtual "end_cash_balance", type: :decimal, precision: 19, scale: 4, as: "((start_cash_balance + ((cash_inflows - cash_outflows) * (flows_factor)::numeric)) + cash_adjustments)", stored: true
- t.virtual "end_non_cash_balance", type: :decimal, precision: 19, scale: 4, as: "(((start_non_cash_balance + ((non_cash_inflows - non_cash_outflows) * (flows_factor)::numeric)) + net_market_flows) + non_cash_adjustments)", stored: true
- t.virtual "end_balance", type: :decimal, precision: 19, scale: 4, as: "(((start_cash_balance + ((cash_inflows - cash_outflows) * (flows_factor)::numeric)) + cash_adjustments) + (((start_non_cash_balance + ((non_cash_inflows - non_cash_outflows) * (flows_factor)::numeric)) + net_market_flows) + non_cash_adjustments))", stored: true
+ t.decimal "non_cash_inflows", precision: 19, scale: 4, default: "0.0", null: false
+ t.decimal "non_cash_outflows", precision: 19, scale: 4, default: "0.0", null: false
+ t.decimal "start_balance", precision: 19, scale: 4
+ t.decimal "start_cash_balance", precision: 19, scale: 4, default: "0.0", null: false
+ t.decimal "start_non_cash_balance", precision: 19, scale: 4, default: "0.0", null: false
+ t.datetime "updated_at", null: false
t.index ["account_id", "date", "currency"], name: "index_account_balances_on_account_id_date_currency_unique", unique: true
t.index ["account_id", "date"], name: "index_balances_on_account_id_and_date", order: { date: :desc }
t.index ["account_id"], name: "index_balances_on_account_id"
end
- create_table "binance_accounts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "binance_item_id", null: false
- t.string "name"
- t.string "account_type"
- t.string "currency"
- t.decimal "current_balance", precision: 19, scale: 4
- t.jsonb "institution_metadata"
- t.jsonb "raw_payload"
- t.jsonb "raw_transactions_payload"
- t.jsonb "extra", default: {}, null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.index ["account_type"], name: "index_binance_accounts_on_account_type"
- t.index ["binance_item_id", "account_type"], name: "index_binance_accounts_on_item_and_type", unique: true
- t.index ["binance_item_id"], name: "index_binance_accounts_on_binance_item_id"
- end
-
- create_table "binance_items", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "family_id", null: false
- t.string "name"
- t.string "institution_name"
- t.string "institution_domain"
- t.string "institution_url"
- t.string "institution_color"
- t.string "status", default: "good"
- t.boolean "scheduled_for_deletion", default: false
- t.boolean "pending_account_setup", default: false
- t.datetime "sync_start_date"
- t.jsonb "raw_payload"
- t.text "api_key"
- t.text "api_secret"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.index ["family_id"], name: "index_binance_items_on_family_id"
- t.index ["status"], name: "index_binance_items_on_status"
- end
-
- create_table "budget_categories", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "budget_id", null: false
- t.uuid "category_id", null: false
+ create_table "budget_categories", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.string "budget_id", null: false
t.decimal "budgeted_spending", precision: 19, scale: 4, null: false
- t.string "currency", null: false
+ t.string "category_id", null: false
t.datetime "created_at", null: false
+ t.string "currency", null: false
t.datetime "updated_at", null: false
t.index ["budget_id", "category_id"], name: "index_budget_categories_on_budget_id_and_category_id", unique: true
t.index ["budget_id"], name: "index_budget_categories_on_budget_id"
t.index ["category_id"], name: "index_budget_categories_on_category_id"
end
- create_table "budgets", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "family_id", null: false
- t.date "start_date", null: false
- t.date "end_date", null: false
+ create_table "budgets", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.decimal "budgeted_spending", precision: 19, scale: 4
- t.decimal "expected_income", precision: 19, scale: 4
- t.string "currency", null: false
t.datetime "created_at", null: false
+ t.string "currency", null: false
+ t.date "end_date", null: false
+ t.decimal "expected_income", precision: 19, scale: 4
+ t.string "family_id", null: false
+ t.date "start_date", null: false
t.datetime "updated_at", null: false
t.index ["family_id", "start_date", "end_date"], name: "index_budgets_on_family_id_and_start_date_and_end_date", unique: true
t.index ["family_id"], name: "index_budgets_on_family_id"
end
- create_table "categories", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.string "name", null: false
+ create_table "categories", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.string "classification", default: "expense", null: false
t.string "color", default: "#6172F3", null: false
- t.uuid "family_id", null: false
t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.uuid "parent_id"
+ t.string "family_id", null: false
t.string "lucide_icon", default: "shapes", null: false
- t.string "classification_unused", default: "expense", null: false
+ t.string "name", null: false
+ t.string "parent_id"
+ t.datetime "updated_at", null: false
t.index ["family_id"], name: "index_categories_on_family_id"
end
- create_table "chats", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "user_id", null: false
- t.string "title", null: false
+ create_table "chats", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.json "error"
t.string "instructions"
- t.jsonb "error"
t.string "latest_assistant_response_id"
- t.datetime "created_at", null: false
+ t.string "title", null: false
t.datetime "updated_at", null: false
+ t.string "user_id", null: false
t.index ["user_id"], name: "index_chats_on_user_id"
end
- create_table "coinbase_accounts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "coinbase_item_id", null: false
- t.string "name"
- t.string "account_id"
- t.string "currency"
- t.decimal "current_balance", precision: 19, scale: 4
- t.string "account_status"
- t.string "account_type"
- t.string "provider"
- t.jsonb "institution_metadata"
- t.jsonb "raw_payload"
- t.jsonb "raw_transactions_payload"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.index ["account_id"], name: "index_coinbase_accounts_on_account_id"
- t.index ["coinbase_item_id", "account_id"], name: "index_coinbase_accounts_on_item_and_account_id", unique: true, where: "(account_id IS NOT NULL)"
- t.index ["coinbase_item_id"], name: "index_coinbase_accounts_on_coinbase_item_id"
- end
-
- create_table "coinbase_items", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "family_id", null: false
- t.string "name"
- t.string "institution_id"
- t.string "institution_name"
- t.string "institution_domain"
- t.string "institution_url"
- t.string "institution_color"
- t.string "status", default: "good"
- t.boolean "scheduled_for_deletion", default: false
- t.boolean "pending_account_setup", default: false
- t.datetime "sync_start_date"
- t.jsonb "raw_payload"
- t.jsonb "raw_institution_payload"
- t.text "api_key"
- t.text "api_secret"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.index ["family_id"], name: "index_coinbase_items_on_family_id"
- t.index ["status"], name: "index_coinbase_items_on_status"
- end
-
- create_table "coinstats_accounts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "coinstats_item_id", null: false
- t.string "name"
- t.string "account_id"
- t.string "currency"
- t.decimal "current_balance", precision: 19, scale: 4
- t.string "account_status"
- t.string "account_type"
- t.string "provider"
- t.jsonb "institution_metadata"
- t.jsonb "raw_payload"
- t.jsonb "raw_transactions_payload"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.string "wallet_address"
- t.index ["coinstats_item_id", "account_id", "wallet_address"], name: "index_coinstats_accounts_on_item_account_and_wallet", unique: true
- t.index ["coinstats_item_id"], name: "index_coinstats_accounts_on_coinstats_item_id"
- end
-
- create_table "coinstats_items", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "family_id", null: false
- t.string "name"
- t.string "institution_id"
- t.string "institution_name"
- t.string "institution_domain"
- t.string "institution_url"
- t.string "institution_color"
- t.string "status", default: "good"
- t.boolean "scheduled_for_deletion", default: false
- t.boolean "pending_account_setup", default: false
- t.datetime "sync_start_date"
- t.jsonb "raw_payload"
- t.jsonb "raw_institution_payload"
- t.string "api_key", null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.string "exchange_portfolio_id"
- t.string "exchange_connection_id"
- t.index ["exchange_connection_id"], name: "index_coinstats_items_on_exchange_connection_id"
- t.index ["family_id", "exchange_portfolio_id"], name: "index_coinstats_items_on_family_id_and_exchange_portfolio_id", unique: true, where: "(exchange_portfolio_id IS NOT NULL)"
- t.index ["family_id"], name: "index_coinstats_items_on_family_id"
- t.index ["status"], name: "index_coinstats_items_on_status"
- end
-
- create_table "credit_cards", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.decimal "available_credit", precision: 10, scale: 2
- t.decimal "minimum_payment", precision: 10, scale: 2
+ create_table "credit_cards", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.decimal "annual_fee", precision: 10, scale: 2
t.decimal "apr", precision: 10, scale: 2
+ t.decimal "available_credit", precision: 10, scale: 2
+ t.datetime "created_at", null: false
t.date "expiration_date"
- t.decimal "annual_fee", precision: 10, scale: 2
- t.jsonb "locked_attributes", default: {}
+ t.json "locked_attributes", default: {}
+ t.decimal "minimum_payment", precision: 10, scale: 2
t.string "subtype"
+ t.datetime "updated_at", null: false
end
- create_table "cryptos", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ create_table "cryptos", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.jsonb "locked_attributes", default: {}
+ t.json "locked_attributes", default: {}
t.string "subtype"
- t.string "tax_treatment", default: "taxable", null: false
+ t.datetime "updated_at", null: false
end
- create_table "data_enrichments", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.string "enrichable_type", null: false
- t.uuid "enrichable_id", null: false
- t.string "source"
+ create_table "data_enrichments", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "attribute_name"
- t.jsonb "value"
- t.jsonb "metadata"
t.datetime "created_at", null: false
+ t.string "enrichable_id", null: false
+ t.string "enrichable_type", null: false
+ t.json "metadata"
+ t.string "source"
t.datetime "updated_at", null: false
+ t.json "value"
t.index ["enrichable_id", "enrichable_type", "source", "attribute_name"], name: "idx_on_enrichable_id_enrichable_type_source_attribu_5be5f63e08", unique: true
t.index ["enrichable_type", "enrichable_id"], name: "index_data_enrichments_on_enrichable"
end
- create_table "depositories", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ create_table "depositories", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.jsonb "locked_attributes", default: {}
+ t.json "locked_attributes", default: {}
t.string "subtype"
- end
-
- create_table "enable_banking_accounts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "enable_banking_item_id", null: false
- t.string "name"
- t.string "account_id"
- t.string "currency"
- t.decimal "current_balance", precision: 19, scale: 4
- t.string "account_status"
- t.string "account_type"
- t.string "provider"
- t.string "iban"
- t.string "uid"
- t.jsonb "institution_metadata"
- t.jsonb "raw_payload"
- t.jsonb "raw_transactions_payload"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.string "product"
- t.decimal "credit_limit", precision: 19, scale: 4
- t.jsonb "identification_hashes", default: []
- t.index ["account_id"], name: "index_enable_banking_accounts_on_account_id"
- t.index ["enable_banking_item_id"], name: "index_enable_banking_accounts_on_enable_banking_item_id"
- t.index ["identification_hashes"], name: "index_enable_banking_accounts_on_identification_hashes", using: :gin
- end
-
- create_table "enable_banking_items", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "family_id", null: false
- t.string "name"
- t.string "institution_id"
- t.string "institution_name"
- t.string "institution_domain"
- t.string "institution_url"
- t.string "institution_color"
- t.string "status", default: "good"
- t.boolean "scheduled_for_deletion", default: false
- t.boolean "pending_account_setup", default: false
- t.date "sync_start_date"
- t.jsonb "raw_payload"
- t.jsonb "raw_institution_payload"
- t.string "country_code"
- t.string "application_id"
- t.text "client_certificate"
- t.string "session_id"
- t.datetime "session_expires_at"
- t.string "aspsp_name"
- t.string "aspsp_id"
- t.string "authorization_id"
- t.datetime "created_at", null: false
t.datetime "updated_at", null: false
- t.jsonb "aspsp_required_psu_headers", default: []
- t.integer "aspsp_maximum_consent_validity"
- t.string "aspsp_auth_approach"
- t.jsonb "aspsp_psu_types", default: []
- t.string "last_psu_ip"
- t.string "psu_type"
- t.index ["family_id"], name: "index_enable_banking_items_on_family_id"
- t.index ["status"], name: "index_enable_banking_items_on_status"
end
- create_table "entries", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "account_id", null: false
- t.string "entryable_type"
- t.uuid "entryable_id"
+ create_table "entries", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.string "account_id", null: false
t.decimal "amount", precision: 19, scale: 4, null: false
+ t.datetime "created_at", null: false
t.string "currency"
t.date "date"
+ t.string "entryable_id"
+ t.string "entryable_type"
+ t.boolean "excluded", default: false
+ t.string "import_id"
+ t.json "locked_attributes", default: {}
t.string "name", null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.uuid "import_id"
t.text "notes"
- t.boolean "excluded", default: false
t.string "plaid_id"
- t.jsonb "locked_attributes", default: {}
- t.string "external_id"
- t.string "source"
- t.boolean "user_modified", default: false, null: false
- t.boolean "import_locked", default: false, null: false
- t.uuid "parent_entry_id"
- t.index "lower((name)::text)", name: "index_entries_on_lower_name"
+ t.datetime "updated_at", null: false
t.index ["account_id", "date"], name: "index_entries_on_account_id_and_date"
t.index ["account_id", "source", "external_id"], name: "index_entries_on_account_source_and_external_id", unique: true, where: "((external_id IS NOT NULL) AND (source IS NOT NULL))"
t.index ["account_id"], name: "index_entries_on_account_id"
+ t.index ["amount"], name: "index_entries_on_amount"
t.index ["date"], name: "index_entries_on_date"
+ t.index ["entryable_id", "entryable_type"], name: "index_entries_on_entryable"
t.index ["entryable_type"], name: "index_entries_on_entryable_type"
+ t.index ["excluded"], name: "index_entries_on_excluded"
t.index ["import_id"], name: "index_entries_on_import_id"
- t.index ["import_locked"], name: "index_entries_on_import_locked_true", where: "(import_locked = true)"
- t.index ["parent_entry_id"], name: "index_entries_on_parent_entry_id"
- t.index ["user_modified"], name: "index_entries_on_user_modified_true", where: "(user_modified = true)"
- end
-
- create_table "eval_datasets", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.string "name", null: false
- t.string "description"
- t.string "eval_type", null: false
- t.string "version", default: "1.0", null: false
- t.integer "sample_count", default: 0
- t.jsonb "metadata", default: {}
- t.boolean "active", default: true
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.index ["eval_type", "active"], name: "index_eval_datasets_on_eval_type_and_active"
- t.index ["name"], name: "index_eval_datasets_on_name", unique: true
+ t.index ["name"], name: "index_entries_on_name"
end
- create_table "eval_results", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "eval_run_id", null: false
- t.uuid "eval_sample_id", null: false
- t.jsonb "actual_output", null: false
- t.boolean "correct", null: false
- t.boolean "exact_match", default: false
- t.boolean "hierarchical_match", default: false
- t.boolean "null_expected", default: false
- t.boolean "null_returned", default: false
- t.float "fuzzy_score"
- t.integer "latency_ms"
- t.integer "prompt_tokens"
- t.integer "completion_tokens"
- t.decimal "cost", precision: 10, scale: 6
- t.jsonb "metadata", default: {}
+ create_table "exchange_rates", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.boolean "alternative_match", default: false
- t.index ["eval_run_id", "correct"], name: "index_eval_results_on_eval_run_id_and_correct"
- t.index ["eval_run_id"], name: "index_eval_results_on_eval_run_id"
- t.index ["eval_sample_id"], name: "index_eval_results_on_eval_sample_id"
- end
-
- create_table "eval_runs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "eval_dataset_id", null: false
- t.string "name"
- t.string "status", default: "pending", null: false
- t.string "provider", null: false
- t.string "model", null: false
- t.jsonb "provider_config", default: {}
- t.jsonb "metrics", default: {}
- t.integer "total_prompt_tokens", default: 0
- t.integer "total_completion_tokens", default: 0
- t.decimal "total_cost", precision: 10, scale: 6, default: "0.0"
- t.datetime "started_at"
- t.datetime "completed_at"
- t.text "error_message"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.index ["eval_dataset_id", "model"], name: "index_eval_runs_on_eval_dataset_id_and_model"
- t.index ["eval_dataset_id"], name: "index_eval_runs_on_eval_dataset_id"
- t.index ["provider", "model"], name: "index_eval_runs_on_provider_and_model"
- t.index ["status"], name: "index_eval_runs_on_status"
- end
-
- create_table "eval_samples", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "eval_dataset_id", null: false
- t.jsonb "input_data", null: false
- t.jsonb "expected_output", null: false
- t.jsonb "context_data", default: {}
- t.string "difficulty", default: "medium"
- t.string "tags", default: [], array: true
- t.jsonb "metadata", default: {}
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.index ["eval_dataset_id", "difficulty"], name: "index_eval_samples_on_eval_dataset_id_and_difficulty"
- t.index ["eval_dataset_id"], name: "index_eval_samples_on_eval_dataset_id"
- t.index ["tags"], name: "index_eval_samples_on_tags", using: :gin
- end
-
- create_table "exchange_rate_pairs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.string "from_currency", null: false
- t.string "to_currency", null: false
- t.date "first_provider_rate_on"
- t.string "provider_name"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.index ["from_currency", "to_currency"], name: "index_exchange_rate_pairs_on_pair_unique", unique: true
- end
-
- create_table "exchange_rates", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.date "date", null: false
t.string "from_currency", null: false
- t.string "to_currency", null: false
t.decimal "rate", null: false
- t.date "date", null: false
- t.datetime "created_at", null: false
+ t.string "to_currency", null: false
t.datetime "updated_at", null: false
+ t.index ["date", "from_currency", "to_currency"], name: "index_exchange_rates_on_date_and_currencies"
t.index ["from_currency", "to_currency", "date"], name: "index_exchange_rates_on_base_converted_date_unique", unique: true
t.index ["from_currency"], name: "index_exchange_rates_on_from_currency"
t.index ["to_currency"], name: "index_exchange_rates_on_to_currency"
end
- create_table "families", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.string "name"
+ create_table "families", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.boolean "auto_sync_on_login", default: true, null: false
+ t.string "country", default: "US"
t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
t.string "currency", default: "USD"
- t.string "locale", default: "en"
- t.string "stripe_customer_id"
- t.string "date_format", default: "%m-%d-%Y"
- t.string "country", default: "US"
- t.string "timezone"
t.boolean "data_enrichment_enabled", default: false
+ t.string "date_format", default: "%m-%d-%Y"
t.boolean "early_access", default: false
- t.boolean "auto_sync_on_login", default: true, null: false
t.datetime "latest_sync_activity_at", default: -> { "CURRENT_TIMESTAMP" }
t.datetime "latest_sync_completed_at", default: -> { "CURRENT_TIMESTAMP" }
- t.boolean "recurring_transactions_disabled", default: false, null: false
- t.integer "month_start_day", default: 1, null: false
- t.string "vector_store_id"
- t.string "moniker", default: "Family", null: false
- t.string "assistant_type", default: "builtin", null: false
- t.string "default_account_sharing", default: "shared", null: false
- t.string "enabled_currencies", array: true
- t.check_constraint "default_account_sharing::text = ANY (ARRAY['shared'::character varying::text, 'private'::character varying::text])", name: "chk_families_default_account_sharing"
- t.check_constraint "month_start_day >= 1 AND month_start_day <= 28", name: "month_start_day_range"
- end
-
- create_table "family_documents", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "family_id", null: false
- t.string "filename", null: false
- t.string "content_type"
- t.integer "file_size"
- t.string "provider_file_id"
- t.string "status", default: "pending", null: false
- t.jsonb "metadata", default: {}
- t.datetime "created_at", null: false
+ t.string "locale", default: "en"
+ t.string "name"
+ t.string "stripe_customer_id"
+ t.string "timezone"
t.datetime "updated_at", null: false
- t.index ["family_id"], name: "index_family_documents_on_family_id"
- t.index ["provider_file_id"], name: "index_family_documents_on_provider_file_id"
- t.index ["status"], name: "index_family_documents_on_status"
end
- create_table "family_exports", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "family_id", null: false
- t.string "status", default: "pending", null: false
+ create_table "family_exports", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false
+ t.string "family_id", null: false
+ t.string "status", default: "pending", null: false
t.datetime "updated_at", null: false
t.index ["family_id"], name: "index_family_exports_on_family_id"
end
- create_table "family_merchant_associations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "family_id", null: false
- t.uuid "merchant_id", null: false
- t.datetime "unlinked_at"
+ create_table "holdings", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.string "account_id", null: false
+ t.decimal "amount", precision: 19, scale: 4, null: false
t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.index ["family_id", "merchant_id"], name: "idx_on_family_id_merchant_id_23e883e08f", unique: true
- t.index ["family_id"], name: "index_family_merchant_associations_on_family_id"
- t.index ["merchant_id"], name: "index_family_merchant_associations_on_merchant_id"
- end
-
- create_table "holdings", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "account_id", null: false
- t.uuid "security_id", null: false
+ t.string "currency", null: false
t.date "date", null: false
- t.decimal "qty", precision: 24, scale: 8, null: false
t.decimal "price", precision: 19, scale: 4, null: false
- t.decimal "amount", precision: 19, scale: 4, null: false
- t.string "currency", null: false
- t.datetime "created_at", null: false
+ t.decimal "qty", precision: 19, scale: 4, null: false
+ t.string "security_id", null: false
t.datetime "updated_at", null: false
t.string "external_id"
t.decimal "cost_basis", precision: 19, scale: 4
@@ -658,181 +307,113 @@
t.index ["security_id"], name: "index_holdings_on_security_id"
end
- create_table "impersonation_session_logs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "impersonation_session_id", null: false
- t.string "controller"
+ create_table "impersonation_session_logs", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "action"
- t.text "path"
- t.string "method"
- t.string "ip_address"
- t.text "user_agent"
+ t.string "controller"
t.datetime "created_at", null: false
+ t.string "impersonation_session_id", null: false
+ t.string "ip_address"
+ t.string "method"
+ t.text "path"
t.datetime "updated_at", null: false
+ t.text "user_agent"
t.index ["impersonation_session_id"], name: "index_impersonation_session_logs_on_impersonation_session_id"
end
- create_table "impersonation_sessions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "impersonator_id", null: false
- t.uuid "impersonated_id", null: false
- t.string "status", default: "pending", null: false
+ create_table "impersonation_sessions", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false
+ t.string "impersonated_id", null: false
+ t.string "impersonator_id", null: false
+ t.string "status", default: "pending", null: false
t.datetime "updated_at", null: false
t.index ["impersonated_id"], name: "index_impersonation_sessions_on_impersonated_id"
t.index ["impersonator_id"], name: "index_impersonation_sessions_on_impersonator_id"
end
- create_table "import_mappings", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.string "type", null: false
- t.string "key"
- t.string "value"
+ create_table "import_mappings", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.boolean "create_when_empty", default: true
- t.uuid "import_id", null: false
- t.string "mappable_type"
- t.uuid "mappable_id"
t.datetime "created_at", null: false
+ t.string "import_id", null: false
+ t.string "key"
+ t.string "mappable_id"
+ t.string "mappable_type"
+ t.string "type", null: false
t.datetime "updated_at", null: false
+ t.string "value"
t.index ["import_id"], name: "index_import_mappings_on_import_id"
t.index ["mappable_type", "mappable_id"], name: "index_import_mappings_on_mappable"
end
- create_table "import_rows", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "import_id", null: false
+ create_table "import_rows", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "account"
- t.string "date"
- t.string "qty"
- t.string "ticker"
- t.string "price"
t.string "amount"
- t.string "currency"
- t.string "name"
t.string "category"
- t.string "tags"
+ t.datetime "created_at", null: false
+ t.string "currency"
+ t.string "date"
t.string "entity_type"
+ t.string "exchange_operating_mic"
+ t.string "import_id", null: false
+ t.string "name"
t.text "notes"
- t.datetime "created_at", null: false
+ t.string "price"
+ t.string "qty"
+ t.string "tags"
+ t.string "ticker"
t.datetime "updated_at", null: false
- t.string "exchange_operating_mic"
- t.string "category_parent"
- t.string "category_color"
- t.string "category_classification"
- t.string "category_icon"
- t.string "resource_type"
- t.boolean "active"
- t.string "effective_date"
- t.text "conditions"
- t.text "actions"
- t.integer "source_row_number", null: false
- t.index ["import_id", "source_row_number"], name: "index_import_rows_on_import_id_and_source_row_number", unique: true
t.index ["import_id"], name: "index_import_rows_on_import_id"
t.check_constraint "source_row_number > 0", name: "chk_import_rows_source_row_number_positive"
end
- create_table "imports", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.jsonb "column_mappings"
- t.string "status"
- t.string "raw_file_str"
- t.string "normalized_csv_str"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.string "col_sep", default: ","
- t.uuid "family_id", null: false
- t.uuid "account_id"
- t.string "type", null: false
- t.string "date_col_label"
+ create_table "imports", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.string "account_col_label"
+ t.string "account_id"
t.string "amount_col_label"
- t.string "name_col_label"
+ t.string "amount_type_inflow_value"
+ t.string "amount_type_strategy", default: "signed_amount"
t.string "category_col_label"
- t.string "tags_col_label"
- t.string "account_col_label"
- t.string "qty_col_label"
- t.string "ticker_col_label"
- t.string "price_col_label"
- t.string "entity_type_col_label"
- t.string "notes_col_label"
+ t.string "col_sep", default: ","
+ t.json "column_mappings"
+ t.datetime "created_at", null: false
t.string "currency_col_label"
+ t.string "date_col_label"
t.string "date_format", default: "%m/%d/%Y"
- t.string "signage_convention", default: "inflows_positive"
+ t.string "entity_type_col_label"
t.string "error"
- t.string "number_format"
t.string "exchange_operating_mic_col_label"
- t.string "amount_type_strategy", default: "signed_amount"
- t.string "amount_type_inflow_value"
- t.integer "rows_to_skip", default: 0, null: false
- t.integer "rows_count", default: 0, null: false
- t.string "amount_type_identifier_value"
- t.text "ai_summary"
- t.string "document_type"
- t.jsonb "extracted_data"
+ t.string "family_id", null: false
+ t.string "name_col_label"
+ t.string "normalized_csv_str"
+ t.string "notes_col_label"
+ t.string "number_format"
+ t.string "price_col_label"
+ t.string "qty_col_label"
+ t.string "raw_file_str"
+ t.string "signage_convention", default: "inflows_positive"
+ t.string "status"
+ t.string "tags_col_label"
+ t.string "ticker_col_label"
+ t.string "type", null: false
+ t.datetime "updated_at", null: false
t.index ["family_id"], name: "index_imports_on_family_id"
end
- create_table "indexa_capital_accounts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "indexa_capital_item_id", null: false
- t.string "name"
- t.string "indexa_capital_account_id"
- t.string "account_number"
- t.string "currency"
- t.decimal "current_balance", precision: 19, scale: 4
- t.string "account_status"
- t.string "account_type"
- t.string "provider"
- t.jsonb "institution_metadata"
- t.jsonb "raw_payload"
- t.string "indexa_capital_authorization_id"
- t.decimal "cash_balance", precision: 19, scale: 4, default: "0.0"
- t.jsonb "raw_holdings_payload", default: []
- t.jsonb "raw_activities_payload", default: []
- t.datetime "last_holdings_sync"
- t.datetime "last_activities_sync"
- t.boolean "activities_fetch_pending", default: false
- t.date "sync_start_date"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.index ["indexa_capital_authorization_id"], name: "idx_on_indexa_capital_authorization_id_58db208d52"
- t.index ["indexa_capital_item_id", "indexa_capital_account_id"], name: "index_indexa_capital_accounts_on_item_and_account_id", unique: true, where: "(indexa_capital_account_id IS NOT NULL)"
- t.index ["indexa_capital_item_id"], name: "index_indexa_capital_accounts_on_indexa_capital_item_id"
- end
-
- create_table "indexa_capital_items", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "family_id", null: false
- t.string "name"
- t.string "institution_id"
- t.string "institution_name"
- t.string "institution_domain"
- t.string "institution_url"
- t.string "institution_color"
- t.string "status", default: "good"
- t.boolean "scheduled_for_deletion", default: false
- t.boolean "pending_account_setup", default: false
- t.datetime "sync_start_date"
- t.jsonb "raw_payload"
- t.jsonb "raw_institution_payload"
- t.string "username"
- t.string "document"
- t.text "password"
+ create_table "investments", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false
+ t.json "locked_attributes", default: {}
+ t.string "subtype"
t.datetime "updated_at", null: false
- t.text "api_token"
- t.index ["family_id"], name: "index_indexa_capital_items_on_family_id"
- t.index ["status"], name: "index_indexa_capital_items_on_status"
end
- create_table "investments", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ create_table "invitations", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.datetime "accepted_at"
t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.jsonb "locked_attributes", default: {}
- t.string "subtype"
- end
-
- create_table "invitations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "email"
+ t.datetime "expires_at"
+ t.string "family_id", null: false
+ t.string "inviter_id", null: false
t.string "role"
t.string "token"
- t.uuid "family_id", null: false
- t.uuid "inviter_id", null: false
- t.datetime "accepted_at"
- t.datetime "expires_at"
- t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "token_digest"
t.index ["email", "family_id"], name: "index_invitations_on_email_and_family_id_pending", unique: true, where: "(accepted_at IS NULL)"
@@ -843,9 +424,9 @@
t.index ["token_digest"], name: "index_invitations_on_token_digest", unique: true, where: "(token_digest IS NOT NULL)"
end
- create_table "invite_codes", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.string "token", null: false
+ create_table "invite_codes", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false
+ t.string "token", null: false
t.datetime "updated_at", null: false
t.string "token_digest"
t.index ["token"], name: "index_invite_codes_on_token", unique: true
@@ -869,171 +450,88 @@
t.index ["family_id"], name: "index_llm_usages_on_family_id"
end
- create_table "loans", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ create_table "loans", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.string "rate_type"
- t.decimal "interest_rate", precision: 10, scale: 3
- t.integer "term_months"
t.decimal "initial_balance", precision: 19, scale: 4
- t.jsonb "locked_attributes", default: {}
+ t.decimal "interest_rate", precision: 10, scale: 3
+ t.json "locked_attributes", default: {}
+ t.string "rate_type"
t.string "subtype"
- end
-
- create_table "lunchflow_accounts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "lunchflow_item_id", null: false
- t.string "name"
- t.string "account_id"
- t.string "currency"
- t.decimal "current_balance", precision: 19, scale: 4
- t.string "account_status"
- t.string "provider"
- t.string "account_type"
- t.jsonb "institution_metadata"
- t.jsonb "raw_payload"
- t.jsonb "raw_transactions_payload"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.boolean "holdings_supported", default: true, null: false
- t.jsonb "raw_holdings_payload"
- t.index ["account_id"], name: "index_lunchflow_accounts_on_account_id"
- t.index ["lunchflow_item_id", "account_id"], name: "index_lunchflow_accounts_on_item_and_account_id", unique: true, where: "(account_id IS NOT NULL)"
- t.index ["lunchflow_item_id"], name: "index_lunchflow_accounts_on_lunchflow_item_id"
- end
-
- create_table "lunchflow_items", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "family_id", null: false
- t.string "name"
- t.string "institution_id"
- t.string "institution_name"
- t.string "institution_domain"
- t.string "institution_url"
- t.string "institution_color"
- t.string "status", default: "good"
- t.boolean "scheduled_for_deletion", default: false
- t.boolean "pending_account_setup", default: false
- t.datetime "sync_start_date"
- t.jsonb "raw_payload"
- t.jsonb "raw_institution_payload"
- t.datetime "created_at", null: false
+ t.integer "term_months"
t.datetime "updated_at", null: false
- t.text "api_key"
- t.string "base_url"
- t.index ["family_id"], name: "index_lunchflow_items_on_family_id"
- t.index ["status"], name: "index_lunchflow_items_on_status"
end
- create_table "merchants", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.string "name", null: false
+ create_table "merchants", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "color"
- t.uuid "family_id"
t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.string "family_id"
t.string "logo_url"
- t.string "website_url"
- t.string "type", null: false
- t.string "source"
+ t.string "name", null: false
t.string "provider_merchant_id"
- t.index ["family_id", "name"], name: "index_merchants_on_family_id_and_name", unique: true, where: "((type)::text = 'FamilyMerchant'::text)"
+ t.string "source"
+ t.string "type", null: false
+ t.datetime "updated_at", null: false
+ t.string "website_url"
+ t.index ["family_id", "name"], name: "index_merchants_on_family_id_and_name"
t.index ["family_id"], name: "index_merchants_on_family_id"
- t.index ["provider_merchant_id", "source"], name: "index_merchants_on_provider_merchant_id_and_source", unique: true, where: "((provider_merchant_id IS NOT NULL) AND ((type)::text = 'ProviderMerchant'::text))"
- t.index ["source", "name"], name: "index_merchants_on_source_and_name", unique: true, where: "((type)::text = 'ProviderMerchant'::text)"
+ t.index ["source", "name"], name: "index_merchants_on_source_and_name"
t.index ["type"], name: "index_merchants_on_type"
end
- create_table "mercury_accounts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "mercury_item_id", null: false
- t.string "name"
- t.string "account_id", null: false
- t.string "currency"
- t.decimal "current_balance", precision: 19, scale: 4
- t.string "account_status"
- t.string "account_type"
- t.string "provider"
- t.jsonb "institution_metadata"
- t.jsonb "raw_payload"
- t.jsonb "raw_transactions_payload"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.index ["mercury_item_id", "account_id"], name: "index_mercury_accounts_on_item_and_account_id", unique: true
- t.index ["mercury_item_id"], name: "index_mercury_accounts_on_mercury_item_id"
- end
-
- create_table "mercury_items", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "family_id", null: false
- t.string "name"
- t.string "institution_id"
- t.string "institution_name"
- t.string "institution_domain"
- t.string "institution_url"
- t.string "institution_color"
- t.string "status", default: "good"
- t.boolean "scheduled_for_deletion", default: false
- t.boolean "pending_account_setup", default: false
- t.datetime "sync_start_date"
- t.jsonb "raw_payload"
- t.jsonb "raw_institution_payload"
- t.text "token"
- t.string "base_url"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.index ["family_id"], name: "index_mercury_items_on_family_id"
- t.index ["status"], name: "index_mercury_items_on_status"
- end
-
- create_table "messages", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "chat_id", null: false
- t.string "type", null: false
- t.string "status", default: "complete", null: false
- t.text "content"
+ create_table "messages", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "ai_model"
+ t.string "chat_id", null: false
+ t.text "content"
t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
t.boolean "debug", default: false
t.string "provider_id"
t.boolean "reasoning", default: false
+ t.string "status", default: "complete", null: false
+ t.string "type", null: false
+ t.datetime "updated_at", null: false
t.index ["chat_id"], name: "index_messages_on_chat_id"
end
- create_table "mobile_devices", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "user_id", null: false
+ create_table "mobile_devices", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.string "app_version"
+ t.datetime "created_at", null: false
t.string "device_id"
t.string "device_name"
t.string "device_type"
- t.string "os_version"
- t.string "app_version"
t.datetime "last_seen_at"
- t.datetime "created_at", null: false
+ t.integer "oauth_application_id"
+ t.string "os_version"
t.datetime "updated_at", null: false
+ t.string "user_id", null: false
+ t.index ["oauth_application_id"], name: "index_mobile_devices_on_oauth_application_id"
t.index ["user_id", "device_id"], name: "index_mobile_devices_on_user_id_and_device_id", unique: true
t.index ["user_id"], name: "index_mobile_devices_on_user_id"
end
create_table "oauth_access_grants", force: :cascade do |t|
- t.string "resource_owner_id", null: false
t.bigint "application_id", null: false
- t.string "token", null: false
+ t.datetime "created_at", null: false
t.integer "expires_in", null: false
t.text "redirect_uri", null: false
- t.string "scopes", default: "", null: false
- t.datetime "created_at", null: false
+ t.string "resource_owner_id", null: false
t.datetime "revoked_at"
+ t.string "scopes", default: "", null: false
+ t.string "token", null: false
t.index ["application_id"], name: "index_oauth_access_grants_on_application_id"
t.index ["resource_owner_id"], name: "index_oauth_access_grants_on_resource_owner_id"
t.index ["token"], name: "index_oauth_access_grants_on_token", unique: true
end
create_table "oauth_access_tokens", force: :cascade do |t|
- t.string "resource_owner_id"
t.bigint "application_id", null: false
- t.string "token", null: false
- t.string "refresh_token"
- t.integer "expires_in"
- t.string "scopes"
t.datetime "created_at", null: false
- t.datetime "revoked_at"
+ t.integer "expires_in"
t.string "previous_refresh_token", default: "", null: false
- t.uuid "mobile_device_id"
+ t.string "refresh_token"
+ t.string "resource_owner_id"
+ t.datetime "revoked_at"
+ t.string "scopes"
+ t.string "token", null: false
t.index ["application_id"], name: "index_oauth_access_tokens_on_application_id"
t.index ["mobile_device_id"], name: "index_oauth_access_tokens_on_mobile_device_id"
t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true
@@ -1042,290 +540,216 @@
end
create_table "oauth_applications", force: :cascade do |t|
+ t.boolean "confidential", default: true, null: false
+ t.datetime "created_at", null: false
t.string "name", null: false
- t.string "uid", null: false
- t.string "secret", null: false
+ t.string "owner_id"
+ t.string "owner_type"
t.text "redirect_uri", null: false
t.string "scopes", default: "", null: false
- t.boolean "confidential", default: true, null: false
- t.datetime "created_at", null: false
+ t.string "secret", null: false
+ t.string "uid", null: false
t.datetime "updated_at", null: false
- t.uuid "owner_id"
- t.string "owner_type"
t.index ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type"
t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true
end
- create_table "oidc_identities", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "user_id", null: false
- t.string "provider", null: false
- t.string "uid", null: false
- t.jsonb "info", default: {}
- t.datetime "last_authenticated_at"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.string "issuer"
- t.index ["issuer"], name: "index_oidc_identities_on_issuer"
- t.index ["provider", "uid"], name: "index_oidc_identities_on_provider_and_uid", unique: true
- t.index ["user_id"], name: "index_oidc_identities_on_user_id"
- end
-
- create_table "other_assets", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ create_table "other_assets", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.jsonb "locked_attributes", default: {}
+ t.json "locked_attributes", default: {}
t.string "subtype"
+ t.datetime "updated_at", null: false
end
- create_table "other_liabilities", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ create_table "other_liabilities", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.jsonb "locked_attributes", default: {}
+ t.json "locked_attributes", default: {}
t.string "subtype"
+ t.datetime "updated_at", null: false
end
- create_table "plaid_accounts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "plaid_item_id", null: false
- t.string "plaid_id", null: false
- t.string "plaid_type", null: false
- t.string "plaid_subtype"
- t.decimal "current_balance", precision: 19, scale: 4
+ create_table "plaid_accounts", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.decimal "available_balance", precision: 19, scale: 4
+ t.datetime "created_at", null: false
t.string "currency", null: false
- t.string "name", null: false
+ t.decimal "current_balance", precision: 19, scale: 4
t.string "mask"
- t.datetime "created_at", null: false
+ t.string "name", null: false
+ t.string "plaid_id", null: false
+ t.string "plaid_item_id", null: false
+ t.string "plaid_subtype"
+ t.string "plaid_type", null: false
+ t.json "raw_investments_payload", default: {}
+ t.json "raw_liabilities_payload", default: {}
+ t.json "raw_payload", default: {}
+ t.json "raw_transactions_payload", default: {}
t.datetime "updated_at", null: false
- t.jsonb "raw_payload", default: {}
- t.jsonb "raw_transactions_payload", default: {}
- t.jsonb "raw_holdings_payload", default: {}
- t.jsonb "raw_liabilities_payload", default: {}
- t.index ["plaid_item_id", "plaid_id"], name: "index_plaid_accounts_on_item_and_plaid_id", unique: true
+ t.index ["plaid_id"], name: "index_plaid_accounts_on_plaid_id", unique: true
t.index ["plaid_item_id"], name: "index_plaid_accounts_on_plaid_item_id"
end
- create_table "plaid_items", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "family_id", null: false
+ create_table "plaid_items", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "access_token"
- t.string "plaid_id", null: false
+ t.json "available_products", default: []
+ t.json "billed_products", default: []
+ t.datetime "created_at", null: false
+ t.string "family_id", null: false
+ t.string "institution_color"
+ t.string "institution_id"
+ t.string "institution_url"
t.string "name"
t.string "next_cursor"
- t.boolean "scheduled_for_deletion", default: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.string "available_products", default: [], array: true
- t.string "billed_products", default: [], array: true
+ t.string "plaid_id", null: false
t.string "plaid_region", default: "us", null: false
- t.string "institution_url"
- t.string "institution_id"
- t.string "institution_color"
+ t.json "raw_institution_payload", default: {}
+ t.json "raw_payload", default: {}
+ t.boolean "scheduled_for_deletion", default: false
t.string "status", default: "good", null: false
- t.jsonb "raw_payload", default: {}
- t.jsonb "raw_institution_payload", default: {}
+ t.datetime "updated_at", null: false
t.index ["family_id"], name: "index_plaid_items_on_family_id"
t.index ["plaid_id"], name: "index_plaid_items_on_plaid_id", unique: true
end
- create_table "properties", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.integer "year_built"
- t.integer "area_value"
+ create_table "properties", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "area_unit"
- t.jsonb "locked_attributes", default: {}
- t.string "subtype"
- end
-
- create_table "recurring_transactions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "family_id", null: false
- t.uuid "merchant_id"
- t.decimal "amount", precision: 19, scale: 4, null: false
- t.string "currency", null: false
- t.integer "expected_day_of_month", null: false
- t.date "last_occurrence_date", null: false
- t.date "next_expected_date", null: false
- t.string "status", default: "active", null: false
- t.integer "occurrence_count", default: 0, null: false
+ t.integer "area_value"
t.datetime "created_at", null: false
+ t.json "locked_attributes", default: {}
+ t.string "subtype"
t.datetime "updated_at", null: false
- t.string "name"
- t.boolean "manual", default: false, null: false
- t.decimal "expected_amount_min", precision: 19, scale: 4
- t.decimal "expected_amount_max", precision: 19, scale: 4
- t.decimal "expected_amount_avg", precision: 19, scale: 4
- t.uuid "account_id"
- t.index ["account_id"], name: "index_recurring_transactions_on_account_id"
- t.index ["family_id", "account_id", "merchant_id", "amount", "currency"], name: "idx_recurring_txns_acct_merchant", unique: true, where: "(merchant_id IS NOT NULL)"
- t.index ["family_id", "account_id", "name", "amount", "currency"], name: "idx_recurring_txns_acct_name", unique: true, where: "((name IS NOT NULL) AND (merchant_id IS NULL))"
- t.index ["family_id", "status"], name: "index_recurring_transactions_on_family_id_and_status"
- t.index ["family_id"], name: "index_recurring_transactions_on_family_id"
- t.index ["merchant_id"], name: "index_recurring_transactions_on_merchant_id"
- t.index ["next_expected_date"], name: "index_recurring_transactions_on_next_expected_date"
+ t.integer "year_built"
end
- create_table "rejected_transfers", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "inflow_transaction_id", null: false
- t.uuid "outflow_transaction_id", null: false
+ create_table "rejected_transfers", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false
+ t.string "inflow_transaction_id", null: false
+ t.string "outflow_transaction_id", null: false
t.datetime "updated_at", null: false
t.index ["inflow_transaction_id", "outflow_transaction_id"], name: "idx_on_inflow_transaction_id_outflow_transaction_id_412f8e7e26", unique: true
t.index ["inflow_transaction_id"], name: "index_rejected_transfers_on_inflow_transaction_id"
t.index ["outflow_transaction_id"], name: "index_rejected_transfers_on_outflow_transaction_id"
end
- create_table "rule_actions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "rule_id", null: false
+ create_table "rule_actions", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "action_type", null: false
- t.string "value"
t.datetime "created_at", null: false
+ t.string "rule_id", null: false
t.datetime "updated_at", null: false
+ t.string "value"
t.index ["rule_id"], name: "index_rule_actions_on_rule_id"
end
- create_table "rule_conditions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "rule_id"
- t.uuid "parent_id"
+ create_table "rule_conditions", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "condition_type", null: false
- t.string "operator", null: false
- t.string "value"
t.datetime "created_at", null: false
+ t.string "operator", null: false
+ t.string "parent_id"
+ t.string "rule_id"
t.datetime "updated_at", null: false
+ t.string "value"
t.index ["parent_id"], name: "index_rule_conditions_on_parent_id"
t.index ["rule_id"], name: "index_rule_conditions_on_rule_id"
end
- create_table "rule_runs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "rule_id", null: false
- t.string "rule_name"
- t.string "execution_type", null: false
- t.string "status", null: false
- t.integer "transactions_queued", default: 0, null: false
- t.integer "transactions_processed", default: 0, null: false
- t.integer "transactions_modified", default: 0, null: false
- t.integer "pending_jobs_count", default: 0, null: false
- t.datetime "executed_at", null: false
- t.text "error_message"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.index ["executed_at"], name: "index_rule_runs_on_executed_at"
- t.index ["rule_id", "executed_at"], name: "index_rule_runs_on_rule_id_and_executed_at"
- t.index ["rule_id"], name: "index_rule_runs_on_rule_id"
- end
-
- create_table "rules", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "family_id", null: false
- t.string "resource_type", null: false
- t.date "effective_date"
+ create_table "rules", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.boolean "active", default: false, null: false
t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.date "effective_date"
+ t.string "family_id", null: false
t.string "name"
+ t.string "resource_type", null: false
+ t.datetime "updated_at", null: false
t.index ["family_id"], name: "index_rules_on_family_id"
end
- create_table "securities", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.string "ticker", null: false
- t.string "name"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ create_table "securities", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "country_code"
- t.string "exchange_mic"
+ t.datetime "created_at", null: false
t.string "exchange_acronym"
- t.string "logo_url"
+ t.string "exchange_mic"
t.string "exchange_operating_mic"
- t.boolean "offline", default: false, null: false
t.datetime "failed_fetch_at"
t.integer "failed_fetch_count", default: 0, null: false
t.datetime "last_health_check_at"
- t.string "website_url"
- t.string "kind", default: "standard", null: false
- t.string "price_provider"
- t.string "offline_reason"
- t.date "first_provider_price_on"
- t.index "upper((ticker)::text), COALESCE(upper((exchange_operating_mic)::text), ''::text)", name: "index_securities_on_ticker_and_exchange_operating_mic_unique", unique: true
+ t.string "logo_url"
+ t.string "name"
+ t.boolean "offline", default: false, null: false
+ t.string "ticker", null: false
+ t.datetime "updated_at", null: false
t.index ["country_code"], name: "index_securities_on_country_code"
t.index ["exchange_operating_mic"], name: "index_securities_on_exchange_operating_mic"
- t.index ["kind"], name: "index_securities_on_kind"
- t.index ["price_provider", "offline_reason"], name: "index_securities_on_price_provider_and_offline_reason"
- t.index ["price_provider"], name: "index_securities_on_price_provider"
- t.check_constraint "kind::text = ANY (ARRAY['standard'::character varying, 'cash'::character varying]::text[])", name: "chk_securities_kind"
+ t.index ["ticker", "exchange_operating_mic"], name: "index_securities_on_ticker_and_exchange_operating_mic_unique", unique: true
end
- create_table "security_prices", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ create_table "security_prices", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.string "currency", default: "USD", null: false
t.date "date", null: false
t.decimal "price", precision: 19, scale: 4, null: false
- t.string "currency", default: "USD", null: false
- t.datetime "created_at", null: false
+ t.string "security_id"
t.datetime "updated_at", null: false
- t.uuid "security_id"
- t.boolean "provisional", default: false, null: false
t.index ["security_id", "date", "currency"], name: "index_security_prices_on_security_id_and_date_and_currency", unique: true
t.index ["security_id"], name: "index_security_prices_on_security_id"
end
- create_table "sessions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "user_id", null: false
- t.string "user_agent"
- t.string "ip_address"
+ create_table "sessions", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.string "active_impersonator_session_id"
t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.uuid "active_impersonator_session_id"
+ t.json "data", default: {}
+ t.string "ip_address"
+ t.json "prev_transaction_page_params", default: {}
t.datetime "subscribed_at"
- t.jsonb "prev_transaction_page_params", default: {}
- t.jsonb "data", default: {}
- t.string "ip_address_digest"
+ t.datetime "updated_at", null: false
+ t.string "user_agent"
+ t.string "user_id", null: false
t.index ["active_impersonator_session_id"], name: "index_sessions_on_active_impersonator_session_id"
t.index ["ip_address_digest"], name: "index_sessions_on_ip_address_digest"
t.index ["user_id"], name: "index_sessions_on_user_id"
end
create_table "settings", force: :cascade do |t|
- t.string "var", null: false
- t.text "value"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.text "value"
+ t.string "var", null: false
t.index ["var"], name: "index_settings_on_var", unique: true
end
- create_table "simplefin_accounts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "simplefin_item_id", null: false
- t.string "name"
+ create_table "simplefin_accounts", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "account_id"
- t.string "currency"
- t.decimal "current_balance", precision: 19, scale: 4
- t.decimal "available_balance", precision: 19, scale: 4
- t.string "account_type"
t.string "account_subtype"
- t.jsonb "raw_payload"
- t.jsonb "raw_transactions_payload"
+ t.string "account_type"
+ t.decimal "available_balance", precision: 19, scale: 4
+ t.datetime "balance_date"
t.datetime "created_at", null: false
+ t.string "currency"
+ t.decimal "current_balance", precision: 19, scale: 4
+ t.json "extra"
+ t.string "name"
+ t.json "org_data"
+ t.json "raw_payload"
+ t.json "raw_transactions_payload"
+ t.string "simplefin_item_id", null: false
t.datetime "updated_at", null: false
- t.datetime "balance_date"
- t.jsonb "extra"
- t.jsonb "org_data"
- t.jsonb "raw_holdings_payload"
t.index ["account_id"], name: "index_simplefin_accounts_on_account_id"
t.index ["simplefin_item_id", "account_id"], name: "idx_unique_sfa_per_item_and_upstream", unique: true, where: "(account_id IS NOT NULL)"
t.index ["simplefin_item_id"], name: "index_simplefin_accounts_on_simplefin_item_id"
end
- create_table "simplefin_items", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "family_id", null: false
+ create_table "simplefin_items", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.text "access_url"
- t.string "name"
+ t.datetime "created_at", null: false
+ t.string "family_id", null: false
t.string "institution_id"
t.string "institution_name"
t.string "institution_url"
- t.string "status", default: "good"
+ t.string "name"
+ t.boolean "pending_account_setup", default: false, null: false
+ t.json "raw_institution_payload"
+ t.json "raw_payload"
t.boolean "scheduled_for_deletion", default: false
- t.jsonb "raw_payload"
- t.jsonb "raw_institution_payload"
- t.datetime "created_at", null: false
+ t.string "status", default: "good"
t.datetime "updated_at", null: false
- t.boolean "pending_account_setup", default: false, null: false
- t.string "institution_domain"
- t.string "institution_color"
- t.date "sync_start_date"
t.index ["family_id"], name: "index_simplefin_items_on_family_id"
t.index ["institution_domain"], name: "index_simplefin_items_on_institution_domain"
t.index ["institution_id"], name: "index_simplefin_items_on_institution_id"
@@ -1333,229 +757,92 @@
t.index ["status"], name: "index_simplefin_items_on_status"
end
- create_table "snaptrade_accounts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "snaptrade_item_id", null: false
- t.string "name"
- t.string "snaptrade_account_id"
- t.string "snaptrade_authorization_id"
- t.string "account_number"
- t.string "brokerage_name"
- t.string "currency"
- t.decimal "current_balance", precision: 19, scale: 4
- t.decimal "cash_balance", precision: 19, scale: 4
- t.string "account_status"
- t.string "account_type"
- t.string "provider"
- t.jsonb "institution_metadata"
- t.jsonb "raw_payload"
- t.jsonb "raw_transactions_payload"
- t.jsonb "raw_holdings_payload", default: []
- t.jsonb "raw_activities_payload", default: []
- t.datetime "last_holdings_sync"
- t.datetime "last_activities_sync"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.boolean "activities_fetch_pending", default: false
- t.date "sync_start_date"
- t.index ["snaptrade_item_id", "snaptrade_account_id"], name: "index_snaptrade_accounts_on_item_and_snaptrade_account_id", unique: true, where: "(snaptrade_account_id IS NOT NULL)"
- t.index ["snaptrade_item_id"], name: "index_snaptrade_accounts_on_snaptrade_item_id"
- end
-
- create_table "snaptrade_items", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "family_id", null: false
- t.string "name"
- t.string "institution_id"
- t.string "institution_name"
- t.string "institution_domain"
- t.string "institution_url"
- t.string "institution_color"
- t.string "status", default: "good"
- t.boolean "scheduled_for_deletion", default: false
- t.boolean "pending_account_setup", default: false
- t.datetime "sync_start_date"
- t.datetime "last_synced_at"
- t.jsonb "raw_payload"
- t.jsonb "raw_institution_payload"
- t.string "client_id"
- t.string "consumer_key"
- t.string "snaptrade_user_id"
- t.string "snaptrade_user_secret"
+ create_table "subscriptions", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.decimal "amount", precision: 19, scale: 4
t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.index ["family_id"], name: "index_snaptrade_items_on_family_id"
- t.index ["status"], name: "index_snaptrade_items_on_status"
- end
-
- create_table "sophtron_accounts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "sophtron_item_id", null: false
- t.string "name", null: false
- t.string "account_id", null: false
t.string "currency"
- t.decimal "balance", precision: 19, scale: 4
- t.decimal "available_balance", precision: 19, scale: 4
- t.string "account_status"
- t.string "account_type"
- t.string "account_sub_type"
- t.datetime "last_updated"
- t.jsonb "institution_metadata"
- t.jsonb "raw_payload"
- t.jsonb "raw_transactions_payload"
- t.string "customer_id", null: false
- t.string "member_id", null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.index ["account_id"], name: "index_sophtron_accounts_on_account_id"
- t.index ["sophtron_item_id"], name: "index_sophtron_accounts_on_sophtron_item_id"
- t.index ["sophtron_item_id", "account_id"], name: "idx_unique_sophtron_accounts_per_item", unique: true
- end
-
- create_table "sophtron_items", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "family_id", null: false
- t.string "name"
- t.string "institution_id"
- t.string "institution_name"
- t.string "institution_domain"
- t.string "institution_url"
- t.string "institution_color"
- t.string "status", default: "good"
- t.boolean "scheduled_for_deletion", default: false
- t.boolean "pending_account_setup", default: false
- t.datetime "sync_start_date"
- t.jsonb "raw_payload"
- t.jsonb "raw_institution_payload"
- t.string "user_id", null: false
- t.string "access_key", null: false
- t.string "base_url"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.index ["family_id"], name: "index_sophtron_items_on_family_id"
- t.index ["status"], name: "index_sophtron_items_on_status"
- end
-
- create_table "sso_audit_logs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "user_id"
- t.string "event_type", null: false
- t.string "provider"
- t.string "ip_address"
- t.string "user_agent"
- t.jsonb "metadata", default: {}, null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.index ["created_at"], name: "index_sso_audit_logs_on_created_at"
- t.index ["event_type"], name: "index_sso_audit_logs_on_event_type"
- t.index ["user_id", "created_at"], name: "index_sso_audit_logs_on_user_id_and_created_at"
- t.index ["user_id"], name: "index_sso_audit_logs_on_user_id"
- end
-
- create_table "sso_providers", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.string "strategy", null: false
- t.string "name", null: false
- t.string "label", null: false
- t.string "icon"
- t.boolean "enabled", default: true, null: false
- t.string "issuer"
- t.string "client_id"
- t.string "client_secret"
- t.string "redirect_uri"
- t.jsonb "settings", default: {}, null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.index ["enabled"], name: "index_sso_providers_on_enabled"
- t.index ["name"], name: "index_sso_providers_on_name", unique: true
- end
-
- create_table "subscriptions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "family_id", null: false
+ t.datetime "current_period_ends_at"
+ t.string "family_id", null: false
+ t.string "interval"
t.string "status", null: false
t.string "stripe_id"
- t.decimal "amount", precision: 19, scale: 4
- t.string "currency"
- t.string "interval"
- t.datetime "current_period_ends_at"
t.datetime "trial_ends_at"
- t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "cancel_at_period_end", default: false, null: false
t.index ["family_id"], name: "index_subscriptions_on_family_id", unique: true
end
- create_table "syncs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.string "syncable_type", null: false
- t.uuid "syncable_id", null: false
- t.string "status", default: "pending"
- t.string "error"
- t.jsonb "data"
+ create_table "syncs", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.datetime "completed_at"
t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.uuid "parent_id"
+ t.json "data"
+ t.string "error"
+ t.datetime "failed_at"
+ t.string "parent_id"
t.datetime "pending_at"
+ t.string "status", default: "pending"
+ t.string "syncable_id", null: false
+ t.string "syncable_type", null: false
t.datetime "syncing_at"
- t.datetime "completed_at"
- t.datetime "failed_at"
- t.date "window_start_date"
+ t.datetime "updated_at", null: false
t.date "window_end_date"
- t.text "sync_stats"
+ t.date "window_start_date"
t.index ["parent_id"], name: "index_syncs_on_parent_id"
t.index ["status"], name: "index_syncs_on_status"
t.index ["syncable_type", "syncable_id"], name: "index_syncs_on_syncable"
end
- create_table "taggings", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "tag_id", null: false
- t.string "taggable_type"
- t.uuid "taggable_id"
+ create_table "taggings", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false
+ t.string "tag_id", null: false
+ t.string "taggable_id"
+ t.string "taggable_type"
t.datetime "updated_at", null: false
t.index ["tag_id"], name: "index_taggings_on_tag_id"
t.index ["taggable_type", "taggable_id"], name: "index_taggings_on_taggable"
end
- create_table "tags", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.string "name"
+ create_table "tags", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "color", default: "#e99537", null: false
- t.uuid "family_id", null: false
t.datetime "created_at", null: false
+ t.string "family_id", null: false
+ t.string "name"
t.datetime "updated_at", null: false
t.index ["family_id"], name: "index_tags_on_family_id"
end
- create_table "tool_calls", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "message_id", null: false
- t.string "provider_id", null: false
+ create_table "tool_calls", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.json "function_arguments"
+ t.string "function_name"
+ t.json "function_result"
+ t.string "message_id", null: false
t.string "provider_call_id"
+ t.string "provider_id", null: false
t.string "type", null: false
- t.string "function_name"
- t.jsonb "function_arguments"
- t.jsonb "function_result"
- t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["message_id"], name: "index_tool_calls_on_message_id"
end
- create_table "trades", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "security_id", null: false
- t.decimal "qty", precision: 24, scale: 8
- t.decimal "price", precision: 19, scale: 10
+ create_table "trades", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
t.string "currency"
- t.jsonb "locked_attributes", default: {}
- t.string "investment_activity_label"
- t.decimal "fee", precision: 19, scale: 4, default: "0.0", null: false
- t.index ["investment_activity_label"], name: "index_trades_on_investment_activity_label"
+ t.json "locked_attributes", default: {}
+ t.decimal "price", precision: 19, scale: 4
+ t.decimal "qty", precision: 19, scale: 4
+ t.string "security_id", null: false
+ t.datetime "updated_at", null: false
t.index ["security_id"], name: "index_trades_on_security_id"
end
- create_table "transactions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ create_table "transactions", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.string "category_id"
t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.uuid "category_id"
- t.uuid "merchant_id"
- t.jsonb "locked_attributes", default: {}
- t.string "kind", default: "standard", null: false
t.string "external_id"
- t.jsonb "extra", default: {}, null: false
- t.string "investment_activity_label"
+ t.string "kind", default: "standard", null: false
+ t.json "locked_attributes", default: {}
+ t.string "merchant_id"
+ t.datetime "updated_at", null: false
t.index ["category_id"], name: "index_transactions_on_category_id"
t.index ["external_id"], name: "index_transactions_on_external_id"
t.index ["extra"], name: "index_transactions_on_extra", using: :gin
@@ -1564,12 +851,12 @@
t.index ["merchant_id"], name: "index_transactions_on_merchant_id"
end
- create_table "transfers", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "inflow_transaction_id", null: false
- t.uuid "outflow_transaction_id", null: false
- t.string "status", default: "pending", null: false
- t.text "notes"
+ create_table "transfers", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false
+ t.string "inflow_transaction_id", null: false
+ t.text "notes"
+ t.string "outflow_transaction_id", null: false
+ t.string "status", default: "pending", null: false
t.datetime "updated_at", null: false
t.index ["inflow_transaction_id", "outflow_transaction_id"], name: "idx_on_inflow_transaction_id_outflow_transaction_id_8cd07a28bd", unique: true
t.index ["inflow_transaction_id"], name: "index_transfers_on_inflow_transaction_id"
@@ -1577,65 +864,56 @@
t.index ["status"], name: "index_transfers_on_status"
end
- create_table "users", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.uuid "family_id", null: false
+ create_table "users", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.boolean "active", default: true, null: false
+ t.boolean "ai_enabled", default: false, null: false
+ t.datetime "created_at", null: false
+ t.string "default_account_order", default: "name_asc"
+ t.string "default_period", default: "last_30_days", null: false
+ t.string "email"
+ t.string "family_id", null: false
t.string "first_name"
+ t.json "goals", default: []
t.string "last_name"
- t.string "email"
- t.string "password_digest"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.string "role", default: "member", null: false
- t.boolean "active", default: true, null: false
+ t.string "last_viewed_chat_id"
t.datetime "onboarded_at"
- t.string "unconfirmed_email"
- t.string "otp_secret"
+ t.json "otp_backup_codes", default: []
t.boolean "otp_required", default: false, null: false
- t.string "otp_backup_codes", default: [], array: true
- t.boolean "show_sidebar", default: true
- t.string "default_period", default: "last_30_days", null: false
- t.uuid "last_viewed_chat_id"
- t.boolean "show_ai_sidebar", default: true
- t.boolean "ai_enabled", default: false, null: false
- t.string "theme", default: "system"
- t.boolean "rule_prompts_disabled", default: false
+ t.string "otp_secret"
+ t.string "password_digest"
+ t.string "role", default: "member", null: false
t.datetime "rule_prompt_dismissed_at"
- t.text "goals", default: [], array: true
- t.datetime "set_onboarding_preferences_at"
+ t.boolean "rule_prompts_disabled", default: false
t.datetime "set_onboarding_goals_at"
- t.string "default_account_order", default: "name_asc"
- t.jsonb "preferences", default: {}, null: false
- t.string "locale"
- t.string "ui_layout"
- t.uuid "default_account_id"
- t.string "webauthn_id"
- t.index ["default_account_id"], name: "index_users_on_default_account_id"
+ t.datetime "set_onboarding_preferences_at"
+ t.boolean "show_ai_sidebar", default: true
+ t.boolean "show_sidebar", default: true
+ t.string "theme", default: "system"
+ t.string "unconfirmed_email"
+ t.datetime "updated_at", null: false
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["family_id"], name: "index_users_on_family_id"
t.index ["last_viewed_chat_id"], name: "index_users_on_last_viewed_chat_id"
- t.index ["locale"], name: "index_users_on_locale"
- t.index ["otp_secret"], name: "index_users_on_otp_secret", unique: true, where: "(otp_secret IS NOT NULL)"
- t.index ["preferences"], name: "index_users_on_preferences", using: :gin
- t.index ["webauthn_id"], name: "index_users_on_webauthn_id", unique: true, where: "(webauthn_id IS NOT NULL)"
+ t.index ["otp_secret"], name: "index_users_on_otp_secret"
end
- create_table "valuations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ create_table "valuations", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.jsonb "locked_attributes", default: {}
t.string "kind", default: "reconciliation", null: false
+ t.json "locked_attributes", default: {}
+ t.datetime "updated_at", null: false
end
- create_table "vehicles", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ create_table "vehicles", id: :string, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.integer "year"
- t.integer "mileage_value"
- t.string "mileage_unit"
+ t.json "locked_attributes", default: {}
t.string "make"
+ t.string "mileage_unit"
+ t.integer "mileage_value"
t.string "model"
- t.jsonb "locked_attributes", default: {}
t.string "subtype"
+ t.datetime "updated_at", null: false
+ t.integer "year"
end
create_table "webauthn_credentials", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
diff --git a/db/seeds.rb b/db/seeds.rb
index 3de4ad5e2a3..50a6c482cc3 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -4,7 +4,9 @@
puts 'Run the following command to create demo data: `rake demo_data:default`' if Rails.env.development?
-Dir[Rails.root.join('db', 'seeds', '*.rb')].sort.each do |file|
- puts "Loading seed file: #{File.basename(file)}"
- require file
+ActiveRecord::Base.transaction do
+ Dir[Rails.root.join('db', 'seeds', '*.rb')].sort.each do |file|
+ puts "Loading seed file: #{File.basename(file)}"
+ require file
+ end
end
diff --git a/docs/api/chats.md b/docs/api/chats.md
index a729468e6de..66399bbbc5e 100644
--- a/docs/api/chats.md
+++ b/docs/api/chats.md
@@ -56,4 +56,12 @@ Errors conform to the shared `ErrorResponse` schema in the OpenAPI document:
}
```
-Common error codes include `unauthorized`, `forbidden`, `feature_disabled`, `not_found`, `unprocessable_entity`, and `rate_limit_exceeded`.
\ No newline at end of file
+Common error codes:
+- `unauthorized` - Invalid or missing authentication
+- `forbidden` - Insufficient permissions or AI not enabled
+- `not_found` - Resource not found
+- `unprocessable_entity` - Invalid request data
+
+## Rate Limits
+
+Chat API endpoints are subject to the standard API rate limits based on your API key tier.
\ No newline at end of file
diff --git a/docs/hosting/docker.md b/docs/hosting/docker.md
index afd462752e6..5ace300ab3d 100644
--- a/docs/hosting/docker.md
+++ b/docs/hosting/docker.md
@@ -95,6 +95,12 @@ 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:
+
+```txt
+ENABLE_YJIT="true"
+```
+
#### Using HTTPS
Assuming you want to access your instance from the internet, you should have secured your URL address with an SSL certificate.
@@ -158,7 +164,6 @@ BINDING=:: bin/dev # IPv6 dual-stack
```
The bundled devcontainer at `.devcontainer/docker-compose.yml` already pins `BINDING: "0.0.0.0"` so Docker port forwarding reaches the app — no manual override needed when using the devcontainer.
-
### Step 4: Run the app
You are now ready to run the app. Start with the following command to make sure everything is working:
diff --git a/mise.toml b/mise.toml
new file mode 100644
index 00000000000..c6aa6a31ed3
--- /dev/null
+++ b/mise.toml
@@ -0,0 +1,3 @@
+[tools]
+# Keep these pins aligned with .ruby-version
+ruby = "3.4.6"
diff --git a/test/controllers/api/v1/accounts_controller_test.rb b/test/controllers/api/v1/accounts_controller_test.rb
index 65e1715cdec..722b821c7db 100644
--- a/test/controllers/api/v1/accounts_controller_test.rb
+++ b/test/controllers/api/v1/accounts_controller_test.rb
@@ -8,22 +8,14 @@ class Api::V1::AccountsControllerTest < ActionDispatch::IntegrationTest
@other_family_user = users(:family_member)
@other_family_user.update!(family: families(:empty))
- @user.api_keys.active.destroy_all
- @api_key = ApiKey.create!(
- user: @user,
- name: "Test Read Key",
- scopes: [ "read" ],
- source: "web",
- display_key: "test_read_#{SecureRandom.hex(8)}"
- )
+ Account.all.each do |account|
+ account.save! # trigger callback to set classification
+ end
- @other_family_user.api_keys.active.destroy_all
- @other_family_api_key = ApiKey.create!(
- user: @other_family_user,
- name: "Other Family Read Key",
- scopes: [ "read" ],
- source: "web",
- display_key: "other_family_read_#{SecureRandom.hex(8)}"
+ @oauth_app = Doorkeeper::Application.create!(
+ name: "Test API App",
+ redirect_uri: "https://example.com/callback",
+ scopes: "read read_write"
)
end
diff --git a/test/controllers/api/v1/base_controller_test.rb b/test/controllers/api/v1/base_controller_test.rb
index af9e7066dba..036b356c7cf 100644
--- a/test/controllers/api/v1/base_controller_test.rb
+++ b/test/controllers/api/v1/base_controller_test.rb
@@ -22,14 +22,6 @@ class Api::V1::BaseControllerTest < ActionDispatch::IntegrationTest
display_key: @plain_api_key,
scopes: [ "read_write" ]
)
-
- # Clear any existing rate limit data
- Redis.new.del("api_rate_limit:#{@api_key.id}")
- end
-
- teardown do
- # Clean up Redis data after each test
- Redis.new.del("api_rate_limit:#{@api_key.id}")
end
test "should require authentication" do
@@ -325,106 +317,6 @@ class Api::V1::BaseControllerTest < ActionDispatch::IntegrationTest
assert_equal "forbidden", response_body["error"]
end
- test "should include rate limit headers on successful API key requests" do
- get "/api/v1/test", headers: { "X-Api-Key" => @plain_api_key }
-
- assert_response :success
- assert_not_nil response.headers["X-RateLimit-Limit"]
- assert_not_nil response.headers["X-RateLimit-Remaining"]
- assert_not_nil response.headers["X-RateLimit-Reset"]
-
- assert_equal "100", response.headers["X-RateLimit-Limit"]
- assert_equal "99", response.headers["X-RateLimit-Remaining"]
- end
-
- test "should increment rate limit count with each request" do
- # First request
- get "/api/v1/test", headers: { "X-Api-Key" => @plain_api_key }
- assert_response :success
- assert_equal "99", response.headers["X-RateLimit-Remaining"]
-
- # Second request
- get "/api/v1/test", headers: { "X-Api-Key" => @plain_api_key }
- assert_response :success
- assert_equal "98", response.headers["X-RateLimit-Remaining"]
- end
-
- test "should return 429 when rate limit exceeded" do
- # Make 100 requests to exhaust the rate limit
- 100.times do
- get "/api/v1/test", headers: { "X-Api-Key" => @plain_api_key }
- assert_response :success
- end
-
- # 101st request should be rate limited
- get "/api/v1/test", headers: { "X-Api-Key" => @plain_api_key }
- assert_response :too_many_requests
-
- response_body = JSON.parse(response.body)
- assert_equal "rate_limit_exceeded", response_body["error"]
- assert_includes response_body["message"], "Rate limit exceeded"
-
- # Check response headers
- assert_equal "100", response.headers["X-RateLimit-Limit"]
- assert_equal "0", response.headers["X-RateLimit-Remaining"]
- assert_not_nil response.headers["X-RateLimit-Reset"]
- assert_not_nil response.headers["Retry-After"]
- end
-
- test "should not apply rate limiting to OAuth requests" do
- # This would need to be implemented based on your OAuth setup
- # For now, just verify that requests without API keys don't trigger rate limiting
- get "/api/v1/test"
- assert_response :unauthorized
-
- # Should not have rate limit headers for unauthorized requests
- assert_nil response.headers["X-RateLimit-Limit"]
- end
-
- test "should provide detailed rate limit information in 429 response" do
- # Exhaust the rate limit
- 100.times do
- get "/api/v1/test", headers: { "X-Api-Key" => @plain_api_key }
- end
-
- # Make the rate-limited request
- get "/api/v1/test", headers: { "X-Api-Key" => @plain_api_key }
- assert_response :too_many_requests
-
- response_body = JSON.parse(response.body)
- assert_equal "rate_limit_exceeded", response_body["error"]
- assert response_body["details"]["limit"] == 100
- assert response_body["details"]["current"] >= 100
- assert response_body["details"]["reset_in_seconds"] > 0
- end
-
- test "rate limiting should be per API key" do
- # Create a second user for independent API keys
- other_user = users(:family_member)
- other_api_key = ApiKey.create!(
- user: other_user,
- name: "Other Test API Key",
- scopes: [ "read" ],
- display_key: "other_rate_test_#{SecureRandom.hex(8)}"
- )
-
- begin
- # Make 50 requests with first API key
- 50.times do
- get "/api/v1/test", headers: { "X-Api-Key" => @plain_api_key }
- assert_response :success
- end
-
- # Should still be able to make requests with second API key
- get "/api/v1/test", headers: { "X-Api-Key" => other_api_key.display_key }
- assert_response :success
- assert_equal "99", response.headers["X-RateLimit-Remaining"]
- ensure
- Redis.new.del("api_rate_limit:#{other_api_key.id}")
- other_api_key.destroy
- end
- end
-
private
def capture_log(&block)
diff --git a/test/controllers/api/v1/transactions_controller_test.rb b/test/controllers/api/v1/transactions_controller_test.rb
index 8ddb0ebf74a..61d96d49d85 100644
--- a/test/controllers/api/v1/transactions_controller_test.rb
+++ b/test/controllers/api/v1/transactions_controller_test.rb
@@ -27,10 +27,6 @@ class Api::V1::TransactionsControllerTest < ActionDispatch::IntegrationTest
display_key: "test_ro_#{SecureRandom.hex(8)}",
source: "mobile" # Use different source to allow multiple keys
)
-
- # Clear any existing rate limit data
- Redis.new.del("api_rate_limit:#{@api_key.id}")
- Redis.new.del("api_rate_limit:#{@read_only_api_key.id}")
end
# INDEX action tests
diff --git a/test/controllers/api/v1/usage_controller_test.rb b/test/controllers/api/v1/usage_controller_test.rb
index 27235826139..e169d51b385 100644
--- a/test/controllers/api/v1/usage_controller_test.rb
+++ b/test/controllers/api/v1/usage_controller_test.rb
@@ -12,14 +12,6 @@ class Api::V1::UsageControllerTest < ActionDispatch::IntegrationTest
scopes: [ "read" ],
display_key: "usage_test_#{SecureRandom.hex(8)}"
)
-
- # Clear any existing rate limit data
- Redis.new.del("api_rate_limit:#{@api_key.id}")
- end
-
- teardown do
- # Clean up Redis data after each test
- Redis.new.del("api_rate_limit:#{@api_key.id}")
end
test "should return usage information for API key authentication" do
@@ -40,14 +32,6 @@ class Api::V1::UsageControllerTest < ActionDispatch::IntegrationTest
assert_equal [ "read" ], response_body["api_key"]["scopes"]
assert_not_nil response_body["api_key"]["last_used_at"]
assert_not_nil response_body["api_key"]["created_at"]
-
- # Check rate limit information
- assert_equal "standard", response_body["rate_limit"]["tier"]
- assert_equal 100, response_body["rate_limit"]["limit"]
- assert_equal 4, response_body["rate_limit"]["current_count"] # 3 test requests + 1 usage request
- assert_equal 96, response_body["rate_limit"]["remaining"]
- assert response_body["rate_limit"]["reset_in_seconds"] > 0
- assert_not_nil response_body["rate_limit"]["reset_at"]
end
test "should require read scope for usage endpoint" do
@@ -68,7 +52,6 @@ class Api::V1::UsageControllerTest < ActionDispatch::IntegrationTest
response_body = JSON.parse(response.body)
assert_equal "insufficient_scope", response_body["error"]
ensure
- Redis.new.del("api_rate_limit:#{api_key_no_read.id}")
api_key_no_read.destroy
end
end
@@ -79,58 +62,4 @@ class Api::V1::UsageControllerTest < ActionDispatch::IntegrationTest
get "/api/v1/usage"
assert_response :unauthorized
end
-
- test "should update usage count when accessing usage endpoint" do
- # Check initial state
- get "/api/v1/usage", headers: { "X-Api-Key" => @api_key.display_key }
- assert_response :success
-
- response_body = JSON.parse(response.body)
- first_count = response_body["rate_limit"]["current_count"]
-
- # Make another usage request
- get "/api/v1/usage", headers: { "X-Api-Key" => @api_key.display_key }
- assert_response :success
-
- response_body = JSON.parse(response.body)
- second_count = response_body["rate_limit"]["current_count"]
-
- assert_equal first_count + 1, second_count
- end
-
- test "should include rate limit headers in usage response" do
- get "/api/v1/usage", headers: { "X-Api-Key" => @api_key.display_key }
- assert_response :success
-
- assert_not_nil response.headers["X-RateLimit-Limit"]
- assert_not_nil response.headers["X-RateLimit-Remaining"]
- assert_not_nil response.headers["X-RateLimit-Reset"]
-
- assert_equal "100", response.headers["X-RateLimit-Limit"]
- assert_equal "99", response.headers["X-RateLimit-Remaining"]
- end
-
- test "should work correctly when approaching rate limit" do
- # Make 98 requests to get close to the limit
- 98.times do
- get "/api/v1/test", headers: { "X-Api-Key" => @api_key.display_key }
- assert_response :success
- end
-
- # Check usage - this should be request 99
- get "/api/v1/usage", headers: { "X-Api-Key" => @api_key.display_key }
- assert_response :success
-
- response_body = JSON.parse(response.body)
- assert_equal 99, response_body["rate_limit"]["current_count"]
- assert_equal 1, response_body["rate_limit"]["remaining"]
-
- # One more request should hit the limit
- get "/api/v1/test", headers: { "X-Api-Key" => @api_key.display_key }
- assert_response :success
-
- # Now we should be rate limited
- get "/api/v1/usage", headers: { "X-Api-Key" => @api_key.display_key }
- assert_response :too_many_requests
- end
end
diff --git a/test/integration/rack_attack_test.rb b/test/integration/rack_attack_test.rb
index 37fc0b65e5d..c6069acc81b 100644
--- a/test/integration/rack_attack_test.rb
+++ b/test/integration/rack_attack_test.rb
@@ -20,4 +20,32 @@ class RackAttackTest < ActionDispatch::IntegrationTest
throttles = Rack::Attack.throttles.keys
assert_includes throttles, "api/requests", "API requests should have rate limiting"
end
+
+ test "scanner-style php paths are blocked" do
+ get "/ok.php"
+
+ assert_response :forbidden
+ assert_equal({ "error" => "Request blocked." }, JSON.parse(response.body))
+ end
+
+ test "nested wordpress scanner paths are blocked" do
+ get "/wp-content/plugins/hellopress/wp_filemanager.php"
+
+ assert_response :forbidden
+ assert_equal({ "error" => "Request blocked." }, JSON.parse(response.body))
+ end
+
+ test "php variant extensions are blocked" do
+ get "/ioxi001.PhP7"
+
+ assert_response :forbidden
+ assert_equal({ "error" => "Request blocked." }, JSON.parse(response.body))
+ end
+
+ test "double-slash wordpress scanner paths are blocked" do
+ get "//wp-links-opml.php"
+
+ assert_response :forbidden
+ assert_equal({ "error" => "Request blocked." }, JSON.parse(response.body))
+ end
end
diff --git a/test/models/balance/chart_series_builder_test.rb b/test/models/balance/chart_series_builder_test.rb
index 7e180ef365e..ec946d34298 100644
--- a/test/models/balance/chart_series_builder_test.rb
+++ b/test/models/balance/chart_series_builder_test.rb
@@ -4,6 +4,9 @@ class Balance::ChartSeriesBuilderTest < ActiveSupport::TestCase
include BalanceTestHelper
setup do
+ Account.all.each do |account|
+ account.save! # trigger callback to set classification
+ end
end
test "balance series with fallbacks and gapfills" do
diff --git a/test/services/api_rate_limiter_test.rb b/test/services/api_rate_limiter_test.rb
deleted file mode 100644
index 8afc6bb9a8e..00000000000
--- a/test/services/api_rate_limiter_test.rb
+++ /dev/null
@@ -1,138 +0,0 @@
-require "test_helper"
-
-class ApiRateLimiterTest < ActiveSupport::TestCase
- setup do
- @user = users(:family_admin)
- # Destroy any existing active API keys for this user
- @user.api_keys.active.destroy_all
-
- @api_key = ApiKey.create!(
- user: @user,
- name: "Rate Limiter Test Key",
- scopes: [ "read" ],
- display_key: "rate_limiter_test_#{SecureRandom.hex(8)}"
- )
- @rate_limiter = ApiRateLimiter.new(@api_key)
-
- # Clear any existing rate limit data
- Redis.new.del("api_rate_limit:#{@api_key.id}")
- end
-
- teardown do
- # Clean up Redis data after each test
- Redis.new.del("api_rate_limit:#{@api_key.id}")
- end
-
- test "should have default rate limit" do
- assert_equal 100, @rate_limiter.rate_limit
- end
-
- test "should start with zero request count" do
- assert_equal 0, @rate_limiter.current_count
- end
-
- test "should not be rate limited initially" do
- assert_not @rate_limiter.rate_limit_exceeded?
- end
-
- test "should increment request count" do
- assert_equal 0, @rate_limiter.current_count
-
- @rate_limiter.increment_request_count!
- assert_equal 1, @rate_limiter.current_count
-
- @rate_limiter.increment_request_count!
- assert_equal 2, @rate_limiter.current_count
- end
-
- test "should be rate limited when exceeding limit" do
- # Simulate reaching the rate limit
- 100.times { @rate_limiter.increment_request_count! }
-
- assert_equal 100, @rate_limiter.current_count
- assert @rate_limiter.rate_limit_exceeded?
- end
-
- test "should provide correct usage info" do
- 5.times { @rate_limiter.increment_request_count! }
-
- usage_info = @rate_limiter.usage_info
-
- assert_equal 5, usage_info[:current_count]
- assert_equal 100, usage_info[:rate_limit]
- assert_equal 95, usage_info[:remaining]
- assert_equal :standard, usage_info[:tier]
- assert usage_info[:reset_time] > 0
- assert usage_info[:reset_time] <= 3600
- end
-
- test "should calculate remaining requests correctly" do
- 10.times { @rate_limiter.increment_request_count! }
-
- usage_info = @rate_limiter.usage_info
- assert_equal 90, usage_info[:remaining]
- end
-
- test "should have zero remaining when at limit" do
- 100.times { @rate_limiter.increment_request_count! }
-
- usage_info = @rate_limiter.usage_info
- assert_equal 0, usage_info[:remaining]
- end
-
- test "should have zero remaining when over limit" do
- 105.times { @rate_limiter.increment_request_count! }
-
- usage_info = @rate_limiter.usage_info
- assert_equal 0, usage_info[:remaining]
- end
-
- test "class method usage_for should work without incrementing" do
- 5.times { @rate_limiter.increment_request_count! }
-
- usage_info = ApiRateLimiter.usage_for(@api_key)
- assert_equal 5, usage_info[:current_count]
-
- # Should not increment when just checking usage
- usage_info_again = ApiRateLimiter.usage_for(@api_key)
- assert_equal 5, usage_info_again[:current_count]
- end
-
- test "should handle multiple API keys separately" do
- # Create a different user for the second API key
- other_user = users(:family_member)
- other_api_key = ApiKey.create!(
- user: other_user,
- name: "Other API Key",
- scopes: [ "read_write" ],
- display_key: "rate_limiter_other_#{SecureRandom.hex(8)}"
- )
-
- other_rate_limiter = ApiRateLimiter.new(other_api_key)
-
- @rate_limiter.increment_request_count!
- other_rate_limiter.increment_request_count!
- other_rate_limiter.increment_request_count!
-
- assert_equal 1, @rate_limiter.current_count
- assert_equal 2, other_rate_limiter.current_count
- ensure
- Redis.new.del("api_rate_limit:#{other_api_key.id}")
- other_api_key.destroy
- end
-
- test "should calculate reset time correctly" do
- reset_time = @rate_limiter.reset_time
-
- # Reset time should be within the current hour
- assert reset_time > 0
- assert reset_time <= 3600
-
- # Should be roughly the time until the next hour
- current_time = Time.current.to_i
- next_window = ((current_time / 3600) + 1) * 3600
- expected_reset = next_window - current_time
-
- assert_in_delta expected_reset, reset_time, 1
- end
-end
diff --git a/test/services/noop_api_rate_limiter_test.rb b/test/services/noop_api_rate_limiter_test.rb
deleted file mode 100644
index 9c7105b12c8..00000000000
--- a/test/services/noop_api_rate_limiter_test.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-require "test_helper"
-
-class NoopApiRateLimiterTest < ActiveSupport::TestCase
- setup do
- @user = users(:family_admin)
- # Clean up any existing API keys for this user to ensure tests start fresh
- @user.api_keys.destroy_all
-
- @api_key = ApiKey.create!(
- user: @user,
- name: "Noop Rate Limiter Test Key",
- scopes: [ "read" ],
- display_key: "noop_rate_limiter_test_#{SecureRandom.hex(8)}"
- )
- @rate_limiter = NoopApiRateLimiter.new(@api_key)
- end
-
- test "should never be rate limited" do
- assert_not @rate_limiter.rate_limit_exceeded?
- end
-
- test "should not increment request count" do
- @rate_limiter.increment_request_count!
- assert_equal 0, @rate_limiter.current_count
- end
-
- test "should always have zero request count" do
- assert_equal 0, @rate_limiter.current_count
- end
-
- test "should have infinite rate limit" do
- assert_equal Float::INFINITY, @rate_limiter.rate_limit
- end
-
- test "should have zero reset time" do
- assert_equal 0, @rate_limiter.reset_time
- end
-
- test "should provide correct usage info" do
- usage_info = @rate_limiter.usage_info
-
- assert_equal 0, usage_info[:current_count]
- assert_equal Float::INFINITY, usage_info[:rate_limit]
- assert_equal Float::INFINITY, usage_info[:remaining]
- assert_equal 0, usage_info[:reset_time]
- assert_equal :noop, usage_info[:tier]
- end
-
- test "class method usage_for should work" do
- usage_info = NoopApiRateLimiter.usage_for(@api_key)
-
- assert_equal 0, usage_info[:current_count]
- assert_equal Float::INFINITY, usage_info[:rate_limit]
- assert_equal Float::INFINITY, usage_info[:remaining]
- assert_equal 0, usage_info[:reset_time]
- assert_equal :noop, usage_info[:tier]
- end
-end