Skip to main content
This guide shows you how to set up, update, and maintain a self-hosted Sure application with Docker Compose.

Prerequisites

  • Docker Engine installed and running
  • Basic familiarity with the command line

Installation

Install Docker

  1. Follow the official Docker installation guide
  2. Start the Docker service on your machine
  3. Verify the installation:
If Docker is set up correctly, this command will succeed.

Create your application directory

Create a directory where your app will run:

Download the Docker Compose file

Download the sample compose file from the Sure repository:
This creates a compose.yml file in your current directory with the default configuration.

Configuration

By default, the compose.example.yml file runs without any configuration. For production deployments or if you’re running outside of a local network, follow these steps to add security.

Email configuration

To enable email notifications and password resets, configure SMTP settings in your .env file:

SSL/TLS options

For SMTP servers with custom SSL certificates or self-signed certificates: Skip TLS verification (not recommended for production):
Use custom CA certificate:
The SSL_CA_FILE option allows you to specify a custom CA certificate file for SSL verification when connecting to SMTP servers with self-signed or internal certificates.

Create an environment file

Create a .env file where Docker will read environment variables:

Generate a secret key

Generate a secret key using one of these methods: With OpenSSL:
Without OpenSSL:
Save the generated key for the next step.

Configure environment variables

Open the .env file in your text editor and add:
Replace the placeholder values with your generated secret key and a secure database password.

Optional: expose Sure with Cloudflare Tunnel

If you want to reach your self-hosted instance from the public internet without opening router ports, you can put a Cloudflare Tunnel in front of the web container.
  1. Create a tunnel in Cloudflare Zero Trust and copy the tunnel token.
  2. Add the token to your .env file:
  1. Edit compose.yml and set:
This tells Sure to generate HTTPS URLs correctly when Cloudflare terminates TLS before forwarding traffic to the container over HTTP.
  1. Add a cloudflared service to your compose.yml:
  1. In the Cloudflare tunnel dashboard, add a public hostname such as sure.example.com and point it at the internal Docker service:
  1. Start the tunnel connector:
Look for a successful tunnel registration message in the logs, then open https://sure.example.com.
Cloudflare should connect to http://web:3000, not https://web:3000. Sure speaks plain HTTP inside Docker and relies on RAILS_ASSUME_SSL to know the original request was HTTPS.
If you enable passkeys or security keys on a public hostname, also set WEBAUTHN_RP_ID and WEBAUTHN_ALLOWED_ORIGINS in .env to match that hostname.
If you only want Cloudflare Tunnel access, do not leave port 3000 broadly exposed to the internet. Keep the host firewall closed or bind the published port more narrowly.

Passkeys and security keys

If people will use passkeys, Touch ID, Windows Hello, or hardware security keys as MFA credentials, pin the WebAuthn relying party settings in .env:
WEBAUTHN_RP_ID should usually be the registrable domain, not a full URL. For example, use example.com for https://sure.example.com. WEBAUTHN_ALLOWED_ORIGINS must include the full origin where people access Sure, including https://.
Changing WEBAUTHN_RP_ID after people register credentials can make existing passkeys and security keys unavailable. Configure it before rollout and keep it stable across reverse proxy, domain, and hostname changes.
See Passkeys and security keys for user setup steps. Passwordless passkey login is enabled by default:
Set it to "false" if you only want registered passkeys and security keys to work as a second factor after email/password sign-in.

Reverse proxy sub-path deployments

Sure can be served under a path prefix by a reverse proxy — for example https://home.example.com/sure instead of the root. The web app works at that address with no extra configuration. When using the macOS desktop app with a sub-path deployment, enter the full mounted URL (including the path prefix) when adding the server. The app probes the health endpoint at the address you enter and walks up to the origin to find the correct mount point, so pasting a deep link such as https://home.example.com/sure/sessions/new also works.
The RAILS_RELATIVE_URL_ROOT environment variable tells Rails that it is mounted under a prefix. Set it to the path portion if your reverse proxy strips the prefix before forwarding, or configure the proxy to preserve the path and leave the variable unset.

Import size variables

By default, Sure NDJSON imports are limited to a fixed maximum file size. Self-hosted admins can raise this limit with:
This applies to both the GUI upload and the API upload paths. If you do not set this variable, the default limit is used.

Encryption keys for bank sync

When you configure a bank sync provider (such as Plaid, SimpleFIN, or Brex) on a self-hosted instance that has not set explicit Active Record encryption keys, Sure shows a warning before you save your credentials:
Encryption keys missing — This self-hosted install has not configured explicit Active Record encryption keys. Configure primary_key, deterministic_key, and key_derivation_salt before saving provider credentials in production.
The trust statement on the provider form also changes to remind you that credentials will not be stored encrypted until you configure those keys. If encryption is not configured at all when you visit Settings > Bank Sync, Sure blocks access and redirects you with the same “Encryption keys missing” message. To resolve the warning, add these three variables to your .env file before saving any provider credentials in production:
You can generate values with openssl rand -hex 32 for each one. Once all three are present, the warning disappears and credentials are stored encrypted.
Encryption is also required to generate a Wise SCA keypair. Without it, the Generate SCA keypair button returns a validation error. See Enabling SCA for details.
Sure can derive these values from SECRET_KEY_BASE automatically, so the warning only appears when none of the three environment variables are explicitly set. If you already set SECRET_KEY_BASE, encryption is still active — you will only see the warning if you want to use dedicated keys instead. See Turning on Active Record encryption after first boot if you need to backfill existing data.

Brand logo variables

Sure uses Brandfetch to show each provider’s own icon on Bank Sync cards, drawers, and Accounts connection cards, as well as merchant and institution logos throughout the app. Set your Brandfetch client ID to enable these logos:
When BRAND_FETCH_CLIENT_ID is not set, Bank Sync cards and Accounts fall back to each provider’s initials on a colored background. You can also toggle high-resolution logos from Settings → Self-Hosting in the UI.

Application name variables

By default, Sure uses “Sure” as the product name and “FOSS” as the brand name throughout the UI, emails, and page titles. You can override both to white-label your instance:
The combined value appears in the footer as © <year>, <BRAND_NAME> <PRODUCT_NAME>, and in the default email sender name. Other UI copy can use the product name or brand name separately.

Market data provider variables

Sure supports multiple securities pricing providers. You can configure them through environment variables or in the UI under Settings > Self-Hosting. See market data providers for details on each provider.
Setting SECURITIES_PROVIDERS as an environment variable takes precedence over the UI setting. Leave it unset to manage providers from the UI only.
TINKOFF_INVEST_API_KEY serves two purposes: it enables T-Invest as a securities price provider when tinkoff_invest is included in SECURITIES_PROVIDERS, and it allows Sure to fetch brand logos for all securities (including MOEX-priced ones) via T-Invest’s CDN. When a token is configured but T-Invest is not enabled for prices, the T-Invest settings block shows a notice: “Not enabled for prices above — T-Invest is used to fetch brand logos for all your securities whenever a token is configured, independent of the checkbox above.” Logo fetching happens regardless of whether T-Invest is selected as a price provider.

Categorization provider variables

By default, Sure categorizes transactions using your configured LLM provider. You can optionally switch to Jev (by TypeSafe), a classification provider that answers typed questions about a transaction with calibrated probabilities. Because Jev only handles classification it is never used for chat, PDF processing, or merchant detection, and is configured independently of LLM_PROVIDER. Jev is a per-family opt-in behind the preview features gate. When the preview gate is open, each family can select their categorization provider from Settings > Self-Hosting > Categorization provider. The ENV override CATEGORIZATION_PROVIDER applies to every family on the instance.
JEV_ENDPOINT must use HTTPS. An HTTP endpoint (other than loopback addresses such as localhost or 127.0.0.1) is rejected because it would transmit the API key and transaction descriptions in cleartext. If a mis-typed or invalid endpoint is configured, Sure falls back to the LLM provider rather than stopping categorization entirely.
See Jev (TypeSafe) for details on the provider, data-sharing considerations, and how to choose between Jev and the LLM path.

Property valuation provider variables

Sure supports three automated valuation model (AVM) providers for property accounts: RentCast and Realie for US properties, and Chimnie for UK residential properties. Configure them through environment variables or in the UI under Settings > Self-Hosting. See property valuations for details on each provider.
RentCast and Realie only support US addresses and return values in USD. Chimnie only supports UK residential properties and returns values in GBP. A property account must be denominated in the provider’s currency for automatic refreshes to run.

On-chain wallet variables

On-chain wallet tracking uses public block explorers by default. You can override any endpoint with your own instance, and tune history and token limits.
To enable crypto pricing for on-chain wallets, include binance_public in SECURITIES_PROVIDERS. If your family currency is not USD, also set EXCHANGE_RATE_PROVIDER to a provider that needs no API key (e.g. frankfurter). See on-chain wallets for details.

Redis Sentinel (high availability)

By default Sure connects to Redis using REDIS_URL. For high-availability deployments that use Redis Sentinel, set REDIS_SENTINEL_HOSTS instead. When that variable is present it takes precedence over REDIS_URL.
If REDIS_SENTINEL_HOSTS is set but no valid hosts can be parsed from it, Sure falls back to REDIS_URL and logs a warning.

Analytics (PostHog)

Sure can send product analytics events to a PostHog instance. This is off by default and only activates when POSTHOG_KEY is set.
POSTHOG_HOST defaults to https://us.i.posthog.com when not set. Point it at your own PostHog instance if you self-host PostHog. When analytics are enabled, Sure registers a sure_version property on every browser event. The value comes from the .sure-version file bundled in the Docker image and matches the release tag (for example, 0.7.5-alpha.10). This lets you filter PostHog events by the exact Sure version that produced them. Sure also enables PostHog’s person profiles for all events, which allows PostHog to enrich events with GeoIP data based on the client IP address. Two optional variables give you finer control over analytics behavior:
POSTHOG_DEVELOPMENT_ENABLED is only read in the development environment. Set it to true if you want analytics events to fire during local development. In production the key presence alone controls whether events are sent. POSTHOG_FEEDBACK_ENABLED controls whether the in-app feedback widget appears. Set it to false to hide the widget on your instance. When POSTHOG_FEEDBACK_ENABLED is true and POSTHOG_KEY is set, Sure emits one web_ui_served_daily event per user per calendar day. The event carries a daily pseudonym derived from the user ID and the current date — no account ID or persistent person profile is created. It also includes the sure_version, whether the user has preview features enabled, and the client IP for GeoIP enrichment. The pseudonym rotates every day, so the event counts daily active web users without linking activity across days. A shared Redis cache prevents duplicate events from parallel sessions or tab reloads; cache loss can allow a repeat event for the same user on the same day.

Running the application

Start the application

Start the app to verify everything is working:
This pulls the official Docker image and starts the app. You’ll see logs in your terminal. Open your browser and navigate to http://localhost:3000. You should see the Sure login screen.

Create your account

On first run, register a new account:
  1. Click “Create your account” on the login page
  2. Enter your email
  3. Enter a password
The first user to register on a fresh instance is automatically assigned the super_admin role. If two registrations happen at the same time, Sure uses a database lock to ensure exactly one of them becomes super_admin and the other becomes admin.

Restrict future signups

After creating your initial admin account, you can control how other people join your self-hosted instance from Settings > Self-Hosting > Onboarding.
  • Open: Anyone can create an account from the registration page.
  • Invite-only: Signups stay enabled, but a valid invite code is required unless a default family is configured for invite-only onboarding.
  • Closed: The registration page is disabled for new signups.
If you do not want additional self-service registrations, switch the instance to Closed after the initial setup.

Run in the background

To run Sure in the background:
  1. Stop the current process with Ctrl+C
  2. Start in detached mode:
Verify it’s running:
Your app is now accessible at http://localhost:3000.

Updating

The Docker image in your compose.yml file controls which version of Sure you’re running:
  • ghcr.io/we-promise/sure:latest - Latest alpha release
  • ghcr.io/we-promise/sure:stable - Latest stable release
You can also pin to a specific version from the packages page.

Update to the latest version

Your app does not automatically update. To update:

Change update channel

To switch between update channels, edit the compose.yml file:
Then restart the app:

Backup service

The Docker Compose configuration includes an optional backup service that automatically backs up your PostgreSQL database and uploads it to any cloud storage provider supported by rclone — including S3, Cloudflare R2, Google Drive, SFTP, and 70+ others.
The backup service only creates PostgreSQL backups. It does not back up local files stored in Sure’s storage directory. If your deployment uses local file storage, back up that directory separately. If you use external object storage such as S3 or R2, make sure that storage is protected with its own backup and retention policy.

What to back up

For a complete recovery plan, make sure you know which of these apply to your deployment:
  • PostgreSQL database: accounts, transactions, settings, users, and metadata
  • Local file storage: uploaded files stored on disk by the app
  • External object storage: uploaded files stored in S3, R2, or another object store
  • Environment and deployment config: your .env, compose.yml, secrets, and any reverse proxy or DNS setup needed to bring the app back online

Enabling backups

The backup service uses Docker Compose profiles and is disabled by default. To enable it:

Configure backup settings

Add the following variables to your .env file to configure the backup destination and schedule. BACKUP_DESTINATION is required; all other variables are optional.
Then configure your rclone storage provider. The example below uses an S3-compatible provider (such as Cloudflare R2):
For providers other than S3 (Google Drive, SFTP, Backblaze B2, etc.), replace the RCLONE_CONFIG_S3_* variables with the equivalent variables for your provider. See the rclone documentation for the full list of supported providers and their configuration variables. The compose.yml maps the S3 variables by default. If you use a different provider, add the corresponding variables to both your .env file and the backup service’s environment block in compose.yml.

Backup schedule options

BACKUP_SCHEDULE accepts standard cron syntax. Some examples:
  • 0 2 * * * — every day at 2 AM (default)
  • 0 * * * * — every hour
  • 0 2 * * 0 — once per week (Sunday at 2 AM)
  • 0 2 1 * * — once per month (1st of the month at 2 AM)

Restore from a PostgreSQL backup

Use this process when you have a SQL dump created by the backup service or with pg_dump.
[!NOTE] If you customized the PostgreSQL username, password, or database name in your .env or compose.yml, replace sure_user and sure_production in the commands below.
  1. Stop the application containers so they do not write to the database during the restore:
  1. Start or keep the database container running:
  1. Locate the backup file in your backup directory, for example /opt/sure-data/backups.
  2. Restore the SQL backup into PostgreSQL:
  1. If your deployment uses local uploaded files, restore them before restarting the app. If you use external object storage, verify that provider’s backup or versioning recovery has completed.

Restore local uploaded files

If your Sure instance stores uploaded files on the local filesystem, restoring the database alone is not enough. You must also restore the app’s storage directory from the matching file backup. The exact host path depends on how you mapped volumes in compose.yml. Restore the same directory that Sure uses for local storage before starting the app containers. If you are using external object storage instead of local disk, restore those files using that provider’s backup or versioning workflow instead. When the database and any uploaded files are restored, restart the app:

Verify the restore

After restoring, check the following:
  • You can sign in successfully
  • Your accounts and transactions appear as expected
  • Uploaded files open correctly, if you use uploads
  • The web and worker containers start cleanly without repeated errors

Verifying backups

Check that backups are running correctly:

SSL certificate configuration

For self-hosted environments using self-signed certificates or custom certificate authorities, Sure provides SSL configuration options.

Environment variables

Add these variables to your .env file:
SSL_CA_FILE: Path to a custom CA certificate bundle (PEM format). Use this when:
  • Your environment uses self-signed certificates
  • You need to trust a custom certificate authority
  • Corporate proxies inject their own certificates
SSL_VERIFY: Controls SSL certificate verification (default: true)
  • Set to false to disable SSL verification (not recommended for production)
  • Only disable verification in development or testing environments
SSL_DEBUG: Set to true to enable verbose SSL logging at startup. Sure logs the active CA file, verification status, and the path of the combined CA bundle it writes to tmp/ssl_ca_bundle.pem. Use this to diagnose certificate trust issues in development or staging environments. If you’re using a custom CA bundle, mount it into your containers in compose.yml:
Restart the application after updating your configuration:

Optional: SSL/TLS Configuration

Sure supports additional SSL/TLS configuration options for secure email delivery and API connections.

Custom CA Certificate

If you’re using a custom Certificate Authority (CA) or self-signed certificates, you can specify a CA file:
This is useful when:
  • Running Sure in a corporate environment with internal CAs
  • Using self-signed certificates for development
  • Connecting to services with custom certificate chains

Skip TLS Verification for Email

For development or testing environments, you can disable TLS verification for the email mailer:
Only disable TLS verification in development or trusted environments. In production, always use proper TLS verification to ensure secure email delivery.
When to use this:
  • Development environments with self-signed certificates
  • Testing email functionality locally
  • Internal mail servers with custom certificates
Production recommendations:
  • Always use valid SSL/TLS certificates in production
  • Use the SSL_CA_FILE option instead of disabling verification
  • Ensure your SMTP server supports STARTTLS

Mailer SSL configuration

For SMTP connections, additional SSL options are available:
SMTP_ENABLE_STARTTLS_AUTO: Enable STARTTLS for SMTP connections (default: true) SMTP_OPENSSL_VERIFY_MODE: SSL verification mode for SMTP
  • none: Skip SSL verification
  • peer: Verify the server certificate (default)

Security considerations

Disabling SSL verification (SSL_VERIFY=false or SMTP_OPENSSL_VERIFY_MODE=none) exposes your application to man-in-the-middle attacks. Only use these settings in trusted networks or development environments.
For production deployments:
  • Use properly signed certificates from a trusted CA
  • Keep SSL_VERIFY=true
  • Use SMTP_OPENSSL_VERIFY_MODE=peer
  • If you must use self-signed certificates, provide a CA bundle via SSL_CA_FILE

Monitoring system health

When the Sidekiq worker container isn’t running, background jobs silently never execute. Balance calculations, net-worth updates, and account syncs stall, and the UI may show zeros or “No balance data available” without explaining why.

Sidekiq queue dashboard credentials

The Sidekiq queue dashboard is available at /sidekiq for signed-in super admins only. You can add a second HTTP basic-auth layer on top of that by setting both of these variables:
Both variables must be set for basic auth to activate. When only one is present, the extra layer is skipped and the dashboard remains accessible to signed-in super admins as normal.

Sidekiq health banner

If Sure detects that Sidekiq is not processing jobs, a warning banner appears above the main content area on every page for super-admins. The banner indicates that data may be stale and links to the system health page. It is positioned so it does not overlap the page header or other admin bars that may be visible at the top of the screen. The banner title, body, and reason message are displayed in the super-admin’s configured interface language. The check is conservative by design:
  • A worker process is considered stale if its heartbeat is older than 2 minutes, which tolerates brief deploy restarts and Redis blips.
  • A queue is considered backed up if its latency exceeds 5 minutes.
The health snapshot is cached for 60 seconds to avoid extra Redis round-trips on every page load. You can override the thresholds and cache TTL with environment variables:

System health page

Super-admins can view live Sidekiq state at Settings → Advanced → System health. The page shows:
  • Worker process count and last heartbeat time
  • Maximum queue latency
  • Job counters (processed, failed, enqueued, retries)
  • Per-queue depth breakdown
The system health page always bypasses the cache and fetches fresh state, so you can confirm a worker restart took effect immediately.

Hosted usage tab

On the two project-operated installations (app.sure.am and demo.sure.am), the system health page shows a Hosted usage tab. The tab is not available on self-hosted instances and does not appear when APP_DOMAIN is set to any other value. The tab loads a read-only snapshot of the database. It does not contact Stripe, run cleanup jobs, or modify any data. The snapshot covers:
  • Summary — trial household and member counts for the last 30 days, trials expiring in the next 7 days, cleanup-eligible households, and local supporter status counts.
  • Recent trial households — up to 100 subscriptions recorded in the last 30 days that retain a trial end date, including converted trials. The table shows retained sign-in session counts, not currently-online users or visit counts. Signing out deletes a session record.
  • Cleanup eligibility — households that match the existing cleanup scope (a paused or trialing subscription ended more than 14 days ago, or no subscription and a household older than 59 days). Matching the scope does not confirm deletion.
  • Local supporter status — current subscription rows with Stripe-shaped customer and subscription IDs, grouped by status. No Stripe request is made; these reflect the local database only.
The tab is loaded lazily and does not affect the initial page load time. Click the Hosted usage tab to fetch the snapshot. The page rejects the tab parameter on installations where the feature is unavailable and falls back to the Background jobs tab.

AI status tab

The system health page has an AI tab at /admin/system_health?tab=ai. When you first open the tab, Sure shows a loading placeholder and then fetches the results in the background. This keeps the page responsive while up to six live probes run. The checks are bounded, non-destructive, and cover the effective AI configuration:
  • LLM probe — Verifies the configured model is available. For standard OpenAI, it queries the models list. For OpenAI-compatible endpoints (Ollama, OpenRouter, etc.), it sends a short chat completion request instead, since many compatible providers do not implement the models list endpoint.
  • Function calling (tools) probe — Asks the configured model for one trivial tool call using the same tools payload the assistant sends for its own function calls. A pass means tool calls work end-to-end. Two distinct failure outcomes are reported separately: if the tools request is rejected as a client error but the same request without tools succeeds, the probe marks tool support as unsupported (common with models on providers like OpenRouter that answer a bare 404); if the endpoint accepted the tools parameter but the model replied with text instead of calling the tool, the probe marks it as tools accepted, but the model called none. The probe uses the same API route the assistant takes — Responses API for hosted OpenAI when supports_responses_endpoint? is true, Chat Completions otherwise — so it always reflects the path live chat follows.
  • PDF text extraction probe — Sends a synthetic PDF to the model and checks that the response correctly identifies the institution name embedded in the document. This probe only runs when the effective provider is OpenAI and the model reports PDF text extraction support. The synthetic document contains no customer data.
  • PDF vision processing probe — Same synthetic PDF check using the vision processing path. Runs for any provider that reports vision-based PDF support, including Anthropic. The synthetic document contains no customer data. If the probe fails with “The pdftoppm renderer (poppler-utils) is not available in this container”, your image predates the fix that bundled poppler-utils; pull the latest image and restart.
  • Vector store probe — For OpenAI vector stores, confirms a list request succeeds without creating or modifying a store. For pgvector, checks that the vector extension is enabled and the chunks table exists.
  • Embedding probe — For pgvector, sends a short test embedding request and verifies the returned vector matches EMBEDDING_DIMENSIONS. The test vector is not stored.
When the pgvector setup is failing, the tab shows a specific alert explaining the cause instead of a generic failure message. The possible alerts and what to do for each are: The tab also shows the active provider, model, endpoint (credentials are always redacted), function calling status, LLM request timeout, health-check probe timeout, PDF text extraction and vision processing status, and pgvector extension and table status. The model settings field links super-admins to this tab and notes that the assistant requires a model with function-calling support. The LLM request timeout and health-check probe timeout are displayed as separate rows because they bound different operations: the LLM request timeout covers normal chat and PDF import calls, while the health-check probe timeout covers only the live checks run on this page. When OPENAI_URI_BASE points to a custom endpoint, the page identifies the provider where possible — Ollama, OpenRouter, Together AI, Kilo, Cloudflare Workers AI or AI Gateway — and labels unrecognized services as Custom endpoint. Results are cached for 60 seconds by default. Click Run checks again to bypass the cache and get fresh results. Failures are written to the system debug log and to Rails.logger; credentials are never included. Failure reason labels and failure code messages are displayed in the super-admin’s configured interface language. You can tune the AI status checks with these environment variables: AI_HEALTH_PROBE_TIMEOUT is also configurable from the Settings UI without a restart. Go to Settings → Self-Hosting, scroll to the Timeouts section under AI Provider, and enter a value in the Health Check Timeout in Seconds field. The environment variable takes precedence over the UI setting when set to a positive value. Raise this value if PDF text or vision probes time out with a cloud model like gpt-4.1 — the PDF probes do a full extraction round trip that can take longer than the 5-second default, even when real PDF imports succeed.

Verifying worker configuration

The checks above all run from the web process. Most AI work — assistant responses, PDF processing, embeddings, auto-categorization, and merchant detection — actually executes in a Sidekiq worker process, which can differ from web in network access, DNS, proxy rules, or the credentials it loaded. This is most common in Kubernetes deployments using workload-specific environment overrides or a Secret updated without a pod restart. A passing web check does not prove a worker can reach the same provider. Click Verify worker configuration on the AI status tab to queue an asynchronous check (WorkerAiHealthCheckJob). It runs the same live probes from inside whichever worker process dequeues it, and the tab lists the result: that worker’s process identity, when it checked in, whether its effective configuration matches what web resolved, and probe outcomes. All labels and status messages in the worker verification section are displayed in the super-admin’s configured interface language. A few things this does and does not prove:
  • One check verifies one worker. Sidekiq does not broadcast a job to every process, so a passing result confirms the named process is healthy, not your whole fleet. With multiple worker replicas, queue the check again to sample another. Only the 5 most recently checked-in distinct processes are kept. A 6th check-in can evict an older entry before its own 15-minute retention window expires. A result older than WorkerAiHealth::STALE_AFTER displays as Stale rather than pass or fail.
  • Worker checks never reuse a web-cached result, or vice versa. The web page’s probes are cached briefly so repeat page loads do not re-hit providers. The worker job bypasses that shared cache so its result always reflects a live call from its own network context, and never leaves an entry the web process could read back as its own.
  • In local development, web and worker results will not show up together. bin/dev runs web and worker as separate OS processes. The development environment uses Sidekiq, so jobs do reach the worker process, but config/environments/development.rb uses a process-local memory store when development caching is enabled, or a null store when it is disabled, rather than a shared Redis cache. With development caching enabled, a worker check result is written to the worker’s own memory cache; with caching disabled, the result is not retained, so the Verify worker configuration button can appear to do nothing from the web process. This is expected; production’s shared Redis store is unaffected.
  • Which settings need a restart depends on how they are set. Provider, model, and API-key settings changed in Settings → Self-Hosting are stored in the database and take effect automatically on both web and worker without a restart. Embedding configuration and anything set only through an environment variable is fixed when each container starts. Changing those requires restarting or recreating both web and worker. The AI status tab labels this distinction next to the worker results.

OpenAI request timeout

The LLM request timeout controls how long each OpenAI-compatible HTTP request is allowed to take before it is cancelled. It applies to auto-categorization, merchant detection, chat completions, PDF processing, and vector-store calls. The default is 60 seconds, which works well for hosted models. Raise it when using slow local models (for example, Ollama on modest hardware). You can configure the timeout in two ways:
  • Environment variable — set OPENAI_REQUEST_TIMEOUT to a number of seconds. This takes precedence over the UI setting.
  • Settings UI — go to Settings > Self-Hosting and enter a value in the Request Timeout in Seconds field under the OpenAI section.
The minimum accepted value is 1 second. If both the environment variable and the UI setting are present, the environment variable wins.

AI response timeout

The response timeout is a whole-turn watchdog for chat. It bounds how long Sure waits for a complete assistant turn — including all tool calls and their results — before giving up. The default is 90 seconds. Size this as a sum of the per-call request timeout across the maximum number of tool-call iterations, plus tool execution and queue wait time: (1 + ASSISTANT_MAX_TOOL_CALL_ITERATIONS) × OPENAI_REQUEST_TIMEOUT. The 90-second default is sized for typical cloud model latency. For local models where each request is slow, either raise this value or lower ASSISTANT_MAX_TOOL_CALL_ITERATIONS. You can configure the timeout in two ways:
  • Environment variable — set AI_RESPONSE_TIMEOUT to a number of seconds. This takes precedence over the UI setting.
  • Settings UI — go to Settings > Self-Hosting and enter a value in the Response Timeout in Seconds field under the OpenAI section.
The minimum accepted value is 30 seconds. If both the environment variable and the UI setting are present, the environment variable wins.

Extra HTTP headers for OpenAI-compatible requests

OPENAI_EXTRA_HEADERS lets you attach custom HTTP headers to every request Sure sends to the OpenAI-compatible endpoint. This is useful for AI gateways that require routing or authentication headers — for example, sending a session identifier to a proxy that tracks usage per conversation. Set the variable to a JSON object of header names and values:
How it works:
  • The value must be a valid JSON object. If it is missing, not an object, or not valid JSON, Sure logs an error and falls back to no extra headers. Chat continues normally.
  • String keys and values are used as-is. Blank values are dropped.
  • If a header value contains the literal {session_id}, Sure substitutes the current chat’s UUID at request time. This identifies requests per conversation rather than per installation. The substitution only happens for chat requests — batch flows such as auto-categorization, merchant detection, and PDF processing never receive session headers.
  • This variable is environment-only. There is no Settings UI equivalent.
The Sidekiq health banner, system health page, and AI status tab are only available in self-hosted mode. Managed deployments do not show these features.

Demo data refresh

Self-hosted super admins can opt into a daily demo family refresh from Settings > Self-Hosting > Demo data refresh. When enabled, Sure automatically regenerates the designated demo family’s data every day — removing all its existing transactions, accounts, and logins and replacing them with freshly generated sample data. All other families on the instance are untouched.

Requirements

  • A running worker process with at least 2 GiB of RAM. The demo generator is memory-intensive and will fail on smaller workers.
  • A disposable demo family that was created with Sure’s built-in demo generator. Do not select a real user’s family.

Setup

  1. Load sample data to create a demo family: go to Settings and use the demo data generator. This creates a family owned by the configured demo email address.
  2. Go to Settings > Self-Hosting > Demo data refresh.
  3. Under Disposable demo family, select the demo family you just created. The dropdown only shows families that own the demo email address and have no super-admin members.
  4. Click Save demo family.
  5. Toggle Enable daily demo family refresh on.
After you save a demo family and enable the toggle, Sure runs the refresh job once a day. The job:
  • Generates a fresh set of demo data under the same demo email address.
  • Retires and destroys the previous demo family.
  • Updates the stored family ID to point at the newly created family.
If the selected family no longer meets the safety checks (for example, a super-admin was added to it), the job skips that run and logs a warning instead of destroying data.

Completion notification email

After each successful refresh, Sure queues a summary email to every super admin on the instance. The email includes:
  • Activity during this period — the number of unique login sessions recorded for the previous demo family and the number of new family accounts created during the same window.
  • Reporting period (UTC) — the start and end timestamps of the 24-hour window the job reviewed.
  • Previous demo family — the name and ID of the previous family, whose deletion is queued separately. The email does not confirm that deletion has finished.
The email is sent to the super admin’s registered email address and uses a plain-text fallback for mail clients that do not render HTML.
The refresh permanently destroys all data in the selected demo family. Select only a disposable family that was created specifically for this purpose.

Running Sure on small (512 MB) hosts

Sure runs comfortably on hosts or containers limited to 512 MB of RAM, which makes it a good fit for the smallest tiers on platforms like Render, Fly.io, or a cheap VPS.

What fits in 512 MB

Measured on a production deploy of the official image with the tuning below:
  • Boot, first-run onboarding, and everyday use (dashboard, transactions, budgets, reports)
  • Small bank syncs and CSV imports
  • Scheduled cron jobs such as exchange-rate refreshes
Steady-state memory sits around 352 MB, leaving comfortable headroom under a 512 MB limit.

What does not fit in 512 MB

  • The demo-data generator (“Load sample/demo data”): this is the one operation that deterministically exceeds 512 MB. On a 512 MB container it climbs to the limit and gets OOM-killed mid-generation. Because the generation runs in the worker, the symptom is a sample-data load that never completes, sometimes with all rows silently rolled back.
  • Very large first-time imports or historical syncs (tens of thousands of rows) can also exceed the limit. Import your history in smaller batches, or temporarily raise the memory limit for the initial import and lower it afterwards.
  • AI features: the assistant needs extra headroom. If you enable AI, run at least 1 GB.

Tuning already in the image

The official image ships with the memory tuning that makes 512 MB viable, so no extra configuration is needed:
  • jemalloc preloaded to reduce memory fragmentation
  • YJIT (Ruby’s JIT) enabled
  • Puma constrained to 1 worker × 3 threads (WEB_CONCURRENCY=1, RAILS_MAX_THREADS=3)
If you run your own process supervisor instead of the official image, set those same values.

Loading sample data on a small host

If you want to load sample data on a 512 MB host:
  1. Raise the worker’s memory limit. The generator runs in the worker process, not the web process. On Render, bump the worker service’s plan; on Docker Compose, raise the worker container’s memory limit.
  2. Apply the change with a fresh deploy or restart of the worker. On Render, plan changes only take effect on the next deploy and do not apply on a plain restart.
  3. Load the sample data, then optionally lower the worker back to the smaller plan with another fresh deploy.

Troubleshooting

Database connection errors

If you encounter ActiveRecord::DatabaseConnectionError on first startup, Docker may have initialized the Postgres database with a different default role from a previous attempt.
The following commands will delete all existing data in your Sure database. Only proceed if you’re comfortable losing this data.
Reset the database:
The last command verifies the issue is fixed.

Turning on Active Record encryption after first boot

Self-hosted Sure can derive Active Record encryption keys from SECRET_KEY_BASE automatically. If you prefer dedicated keys, you can add:
Keep the same SECRET_KEY_BASE for an existing instance unless you are restoring from a backup that was created with the new value. Sure uses it to derive encryption keys when the dedicated ACTIVE_RECORD_ENCRYPTION_* variables are not set. If SECRET_KEY_BASE changes, deterministic encrypted fields such as user email addresses may no longer be searchable, and email/password login can fail with “invalid username or password” even when the password is correct. If you add those keys after your instance already has data, the normal app startup path is not enough to rewrite older plaintext values:
  • db:prepare still runs automatically on startup for schema setup and migrations.
  • New writes use encryption after the app boots with the keys.
  • Existing rows are not automatically re-encrypted.
Run the backfill task once after the app is up:
The task is idempotent, so it is safe to rerun if needed. If login started failing immediately after changing encryption settings, restore the previous SECRET_KEY_BASE or previous ACTIVE_RECORD_ENCRYPTION_* values first. After the app can read the existing data again, run the backfill task if you are intentionally moving to dedicated keys.

Sure NDJSON import file size limit

By default, Sure NDJSON imports are limited to a fixed maximum file size. Self-hosted admins can raise this limit with an environment variable:
This setting applies to both the GUI upload and the API upload paths. Set the value to the maximum file size in megabytes that you want to allow.

Slow CSV imports

If CSV imports are processing rows slower than expected, check your worker logs for errors:
Look for connection timeouts or Redis communication failures. The sure-worker container requires Redis to process CSV imports.

AI features, external assistant, and Pipelock

Sure ships a separate compose file for AI-related features: compose.example.ai.yml. It adds:
  • Pipelock (always on): AI agent security proxy for outbound tunnel controls and inbound MCP scanning
  • Ollama + Open WebUI (optional --profile local-ai): local LLM inference

Using the AI compose file

Setting up the external AI assistant

The external assistant delegates chat to a remote AI agent instead of calling LLMs directly. The agent calls back to Sure’s /mcp endpoint for financial data (accounts, transactions, balance sheet).
  1. Set the MCP endpoint credentials in your .env:
  1. Set the external assistant connection:
  1. Choose how to activate:
    • Per-family (UI): Go to Settings > Self-Hosting > AI Assistant, select “External”
    • Global (env): Set ASSISTANT_TYPE=external to force all families to use external
If you connect your own agent via MCP and want to remove the built-in chat UI entirely, set ASSISTANT_TYPE=mcp_only (or select “None in app (connect your own agent via MCP)” in the UI). This disables the in-app chat, chat API endpoints, bills AI shortcuts, and AI-written insight text. The /mcp endpoint and background auto-categorization stay active.
  1. Select an agent. After you save a valid URL and token, Sure calls /v1/models on the endpoint and populates an agent dropdown in the UI. Select the agent you want to use and save. The external service manages the model behind each agent; Sure only handles routing. To skip the UI and pin an agent via environment variable, set:
    When EXTERNAL_ASSISTANT_MODEL is set, the dropdown in the UI is disabled and the environment value is always used.
To use the bundled OpenClaw service instead of a separately hosted agent, start the external-assistant profile. This profile starts OpenClaw without the local Ollama or Open WebUI services:
Connection resilience: When Sure cannot reach the external agent before any response has started streaming, it retries the request up to 2 times with exponential backoff (1 second, then 2 seconds) on transient network errors: connection timeouts, read timeouts, refused connections, reset connections, unreachable hosts, and DNS failures. If the agent starts streaming a response and then the connection drops, Sure does not retry — it raises an interruption error immediately so the partial response is not silently discarded. See the docs/hosting/ai.md file in the Sure repository for full configuration details including agent model selection, session keys, and email allowlisting.

Pipelock security proxy

Pipelock sits between Sure and external services and provides:
  • MCP request and response scanning for DLP, prompt injection, and tool poisoning
  • HTTPS tunnel controls for destination, SSRF, rate, budget, CONNECT headers, and optional signed receipts
When using compose.example.ai.yml, Pipelock is always running. External AI agents should connect to port 8889 (Pipelock’s MCP reverse proxy) instead of directly to Sure’s /mcp on port 3000.
The example compose file does not enable TLS interception, so Pipelock controls the tunnel destination but cannot read encrypted request or response bodies. Docker Compose also does not prevent a client from bypassing the proxy.
For full Pipelock configuration including signed receipts, see Self-hosting with Helm for Kubernetes options and the pipelock.example.yaml reference file in the Sure repository.

Local development bind

For bin/dev on your own machine, the server defaults to localhost (127.0.0.1 and [::1]), reachable only from the same machine. If you need external access (phone on the same Wi-Fi, devcontainer port forwarding, LAN testing), set the BINDING environment variable: Procfile.dev starts three processes: the Rails web server, the Tailwind CSS watcher, and a Sidekiq worker. The worker process receives background jobs via Redis, as in production. Tests use the separate Active Job test adapter. Auto-categorization, syncs, and other background work all route through this worker when developing locally.
The bundled devcontainer at .devcontainer/docker-compose.yml already sets BINDING: "0.0.0.0", so Docker port forwarding works without a manual override when using the devcontainer.

GitHub Codespaces

When developing inside a GitHub Codespace, the app accepts login requests from the Codespaces preview URL without any extra configuration. The app detects the standard Codespaces environment variables (CODESPACES, CODESPACE_NAME, and GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN) that GitHub sets automatically and narrows the allowed CSRF origin to the forwarded preview domain. The origin check applies only when Rails.env.development? and CODESPACES=true are both set, so it has no effect in staging or production. The Codespaces initializer also re-declares the cookie store with SameSite=None; Secure so the VS Code Preview iframe can read the session cookie. It uses the application-wide session cookie key (_sure_session, defined as Sure::SESSION_COOKIE_KEY in config/application.rb) rather than a hardcoded name, keeping the cookie name consistent whether the app boots with or without Codespaces. If you cannot log in through a Codespaces preview URL, confirm that the three environment variables above are present in your Codespace environment and that the app is running with CODESPACES=true.

iOS push notifications

Push notifications are available in hosted mode only. When SELF_HOSTED=true or SELF_HOSTING_ENABLED=true is set, push registration and delivery are disabled.

Environment variables

Set the following variables for both the web and worker services: APNS_PRIVATE_KEY_BASE64 is a server-side secret. Do not embed it in the iOS app, logs, or version control. Both web and worker must share the same REDIS_URL (or REDIS_SENTINEL_HOSTS for high-availability setups). A process-local memory cache cannot coordinate the push diagnostics between processes.

Sending a test notification

  1. Sign in to the Sure iOS app with the same account as the super-admin signed in to the web app, and enable push notifications in the app.
  2. Open System Health → Background jobs → Push notifications in the web app.
  3. Click Send test push notification. The test is sent only to the current account’s recently registered iOS devices.
  4. Click Refresh results to see each device’s outcome. Results remain for 24 hours. Accepted by APNs confirms Apple’s acceptance; verify presentation on a real device to confirm delivery. The results table is keyboard-navigable and can be scrolled with the keyboard when it overflows.
The button is disabled when there is no current registration, APNs is unconfigured, delivery is disabled (APNS_ENABLED=false), or another test was requested in the last 30 seconds.

Troubleshooting

Getting help

If you find bugs or have feature requests: