A private, self-hosted dashboard for your spreadsheet-powered finances.
Try it out | Connect a spreadsheet | Run on Linux | Develop
Portico turns spreadsheet data into focused views for income, spending, subscriptions, budgets, net worth, financial safety, financial independence, and data health. The app reads your data but does not change it.
Use either a remote spreadsheet or local CSV files with the required tables and columns. The current remote connection reads Google Sheets. The table layout is compatible with the Tiller Foundation Template, but Portico uses the table contract rather than a provider-specific account.
The repository includes synthetic data, so you can explore every dashboard without a remote spreadsheet.
On the Home page, Financial safety comes first. The net-worth trend and its What changed chart appear together. Each account group pairs its balance-history sparkline with a signed What changed chart, followed by visible account, balance, and change details; institution and updated columns are not shown.
These screenshots use the committed synthetic data. They contain no personal financial records.
Use the interactive demo to explore expense, income, investment, real-estate, and retirement streams.
Install Docker Engine. Then pull the current release and start the demo:
docker pull ghcr.io/nccurry/portico:1.4.0
docker run --rm --init --name portico \
--read-only --tmpfs /tmp:size=64m,mode=1777 \
--cap-drop ALL --security-opt no-new-privileges:true \
--env PORTICO_CONFIG_PATH=/app/portico-demo.toml \
--publish 127.0.0.1:8501:8501 \
ghcr.io/nccurry/portico:1.4.0Open http://127.0.0.1:8501. The local demo accepts connections only from the local computer. It does not load Streamlit secrets or contact a remote spreadsheet.
Press Ctrl+C to stop the demo.
Portico uses a remote spreadsheet by default. The included remote connection uses Google Sheets and does not require a service account. You can instead use local CSV files; see Use local CSV data.
Clone the repository to get the configuration and secrets templates:
git clone https://github.com/nccurry/portico.git
cd porticoCreate these four tabs:
- Transactions
- Balance History
- Categories
- Accounts
Any workbook works when its columns match the schema in this README.
For each tab, set General access to Anyone with the link and select the Viewer role. Anyone who gets a sheet URL can read that sheet. Treat each URL as private.
Copy the secrets template:
cp .streamlit/secrets.example.toml .streamlit/secrets.toml
chmod 600 .streamlit/secrets.tomlThe container runs as Linux user and group ID 1000, which matches the first
normal user on most Linux systems. The file must be owned by that user so the
container can read it without making it public to other host users.
Open .streamlit/secrets.toml. Replace each example URL with the full URL for
the matching tab. Each URL must use https://docs.google.com and include the
numeric gid for that tab.
Never commit .streamlit/secrets.toml. Git ignores this file by default.
Edit config.toml directly. It is a complete configuration file, not an
overlay. Replace its selectors with exact values from your spreadsheet:
nano config.tomlSet the exact image release in .env, then check the workbook:
docker volume create portico-state
cp .env.example .env
docker compose pull
docker compose run --rm app python -m scripts.doctorThe check reads each sheet and validates its basic structure. It does not print sheet URLs or financial rows.
Start Portico:
docker compose up --detachOpen http://127.0.0.1:8501.
Portico 1.3 removes Discord's private category list. Before upgrading, back up
config.toml and .streamlit/secrets.toml, then move the selection into the
existing named transaction sets:
[weekly_summary]
watched_transaction_sets = ["discretionary"]
average_weeks = 48
rolling_weeks = 4
top_merchant_count = 3Remove notifications.discord.categories from .streamlit/secrets.toml. Keep
the portico-state volume: it preserves delivered-period records and prevents
the new Compose service from sending a duplicate summary.
Column names are case-sensitive. Portico ignores unknown columns.
Required columns:
Date, Category, Amount, Account, Month, Week, Full Description,
Institution, Account #, Date Added, and Categorized Date.
Dates must use a format that pandas can read. Amounts can contain dollar signs and commas. Expenses normally use negative values and income uses positive values.
Required columns:
Date, Time, Balance, Account, Account #, Account ID, Institution,
Class, Month, Week, and Date Added.
Portico also reads account type, status, and group columns when they exist.
Required columns:
Category, Group, Type, and Hide From Reports.
Portico treats later columns with date names as monthly budget columns. Budget values can contain dollar signs and commas.
The sheet must contain at least four columns. Portico treats the first four as
Account, Class Override, Group, and Hide. It ignores later columns.
The container is the supported deployment method. It runs as a non-root user, uses a read-only filesystem, and includes a health check.
Show service and health status:
docker compose psFollow the logs:
docker compose logs --follow appStop and remove the container:
docker compose downUpdate Portico:
docker compose pull
docker compose up --detachSet PORTICO_IMAGE in .env to the exact release you want to run. Compose
never relies on a mutable latest image tag.
Portico stores configuration and secrets on the host. The portico-state
volume records successful Discord delivery periods so an update does not send a
duplicate report.
The default address is 127.0.0.1:8501. Only the Linux host can connect to this
address.
To use a different host port, set this in .env:
PORTICO_PUBLISH_PORT=8601To accept connections from a trusted local network, set this in .env:
PORTICO_PUBLISH_ADDRESS=0.0.0.0Portico has no login screen. Do not forward its port to the public internet. A public deployment requires an authenticated TLS reverse proxy.
Copy .env.example to .env to keep the image tag, network address, timezone,
and Discord schedule in one deployment file.
config.toml is the normal application configuration and the
field reference. It shows every supported setting, including empty selectors.
Edit it directly and keep it complete. Portico never loads or merges a second
configuration file during a normal run.
portico-demo.toml is a separate complete configuration
for the committed synthetic data. The demo entry points select it explicitly.
The app shows the demo banner only when the selected file is named exactly
portico-demo.toml.
Portico stops with an error for unknown keys, wrong types, duplicate values, and values outside the supported ranges. Restart Portico after changing a TOML file. Reporting periods use the latest date in the loaded spreadsheet data; there is no manual reference-date setting.
Use exact values from your spreadsheet in config.toml. These are the main
settings you may want to change:
| Section | Setting | What it controls |
|---|---|---|
lookback |
lookback_months |
Calendar-month choices shown on income, spending, and merchant pages. Use 2β5 ascending values. |
lookback |
default_lookback_months |
Initially selected reporting period. It must appear in lookback_months. |
data |
source, directory |
Select remote for the configured remote spreadsheet or local for local CSV files. Local requires directory; remote leaves it empty. |
transaction_sets.<key> |
label, groups, categories, accounts, merchants, transactions_like, includes, excludes |
Defines one reusable expense policy. Direct selectors and included sets are combined; excluded sets are removed last. A set with neither direct selectors nor includes means every expense row. Groups, categories, and accounts are exact sheet values; merchants use the shared merchant aliases; transactions_like is case-insensitive literal text in Full Description. |
filter_sets.<key> |
options, default |
Lists the named transaction sets offered by a page. spending is shared by the category and merchant pages; year_over_year can expose a different set of choices. |
income_savings |
default_view |
Start income and savings in regular or actual view. |
income_savings |
exclude_categories, exclude_groups |
One-off activity removed from the Regular calculation. |
income_savings |
target_rate |
Savings-rate target shown on the income page. |
thresholds |
expense, income |
Default limits offered by the large-transaction filters. |
budget |
history_months |
Months used for budget history and trailing results. |
subscriptions |
known_categories, detection_excluded_categories |
Exact Categories-sheet values used for the known inventory and excluded from automatic discovery. |
subscriptions |
minimum_confidence, stale_after_days |
Discovery cutoff and stale-data warning. |
subscriptions |
default_exclude_categories |
Categories selected by default in Additional discovery exclusions. |
data_health |
stale_account_days |
Age at which an account balance is stale. |
data_health |
duplicate_require_same_* |
Initial duplicate-detection matching rules. |
financial_independence |
Return, history, projection, earned-income, investment, real-estate, and retirement-stream settings | Home-page FI funding progress and Financial independence source defaults. |
financial_safety |
Emergency-fund target, expense baseline, liquid-account scope, and debt baseline | Home-page safety progress. Emergency spending uses complete months only. The Home time frame takes precedence over debt_baseline_date. |
weekly_summary |
watched_transaction_sets, average_weeks, rolling_weeks, top_merchant_count |
Named transaction sets, comparison windows, and merchant detail for Discord. |
merchants.aliases |
Merchant name and description fragments | Combine several transaction descriptions under one merchant name. |
Financial-safety account patterns match the Account name in Balance History, not the composite label or account number in the Accounts sheet. Debt decreases show a green downward arrow; increases show a red upward arrow and the percentage above the starting balance. Debt change compares the latest recorded balances at the start and end of the selected Home time frame; All uses the full available history. The baseline includes only balances recorded by that date, so accounts first recorded later can increase the comparison total.
The View controls choose among the configured transaction sets. Other page controls can narrow that set for exploration, but cannot broaden it. Those choices last for the browser session only.
The Financial independence page separates data setup from the plan you can adjust. Set Years to project at the top of Your plan. Income and expenses share one row when Income is included; expenses use the full row otherwise. Open Configure streams with the settings icon in Your plan to choose an expense period and exclusions, whether income comes from transactions, whether income or expenses change over time, and the accounts behind Investments and Real estate. Include in plan chooses which of Income, Investments, Real estate, Social Security, and Pension affect the plan and charts.
Expenses start as one yearly amount. Turn on Change expenses over time in Configure streams to replace that field with one list of expense periods. The first row starts in year 1; select Add expense period for each later amount and the year it begins. You can remove later periods or turn the option off to return to one amount. Runway, coverage, and sensitivity use the full schedule; the yearly gap and amount needed from assets use year 1 expenses. All amounts are in today's dollars.
When Income is included, Change income over time works the same way: year 1 starts with one earned-income amount, and each added period replaces it from its selected year onward. Enter $0 for a year when earned income stops. Social Security and pensions are separate streams and are added on top. Projections, coverage, and runway use every income period; the yearly gap and amount needed from assets use year 1 income.
Your expenses and income determine how much the plan draws from assets. There
is no separate withdrawal-rate setting or rate-based investment target. If you
have a custom config.toml from an earlier version, remove its
withdrawal_rate line before upgrading; unknown settings are rejected.
Runway shows Covered when income and assets pay expenses in every selected projection year, even if they would run out later. Sustainable means the model does not project an end to that funding.
Money fields show dollar signs and commas. You can type $100,000 or 100000;
the app formats the value after you finish editing.
default_active_streams sets which streams are selected when the page opens
or you reset your plan. Use any of Income, Investments, Real estate,
Social Security, and Pension.
social_security_* and pension_* in config.toml provide a yearly amount
and the number of years until each benefit starts. These values use today's
dollars. You can change them for the current browser session without changing
your configuration file. The Social Security default is $25,032 per year,
based on the SSA's July 2026 average retired-worker benefit of $2,085.98 per
month.
It is a general starting point, not a personal benefit estimate.
real_estate_monthly_cash_flow is the money left from a property each month
after its costs (and can be negative). real_estate_appreciation_rate is the
expected yearly property growth after inflation. Both appear under Real estate
in Your plan when that stream is included.
The projection grows Investments at the investment rate and Real estate at the property growth rate. Income left after expenses is added to Investments at year-end, even when the current investment balance is excluded. It starts earning the investment rate the following year. If Investments run out, the model uses property value to cover the remaining expenses. It does not estimate sale costs or taxes.
How expenses are covered groups consecutive years that have the same mix of income and asset use. Red shows expenses the plan cannot cover. Income above expenses is saved, not counted as expense coverage.
The other starting assumptions come from [financial_independence]. Account
balances, expenses, and income still come from the data sources you choose.
Local CSV is a first-class source, not a separate application mode. Edit the
same config.toml file and use the four exported files
transactions.csv, balance_history.csv, categories.csv, and accounts.csv:
[data]
source = "local"
directory = "/data"directory may be absolute or relative to config.toml. The reports use the
latest date in the local files, just as they do for a remote spreadsheet.
portico-demo.toml is the complete configuration for
demo/data.
For a Compose deployment, add the CSV directory in an ignored
compose.override.yaml next to compose.yaml:
services:
app:
volumes:
- ./data:/data:rocompose.yaml is the supported deployment manifest. It mounts your complete
config.toml and .streamlit/secrets.toml as individual read-only files. Do
not mount a configuration directory over /app.
Compose recreates the service when the image or environment changes. After changing either TOML file, recreate it explicitly:
docker compose up --detach --force-recreateThe named portico-state volume remains available.
These environment variables change the main application settings:
| Variable | Use |
|---|---|
PORTICO_DISCORD_ENABLED |
Set to true to enable scheduled Discord summaries. The default is false. |
PORTICO_DISCORD_CRON |
Set the five-field cron schedule. The default is 0 9 * * 0 (Sunday at 9:00 AM). |
TZ |
Set the IANA timezone used by the Discord schedule, such as America/Chicago. |
Keep remote spreadsheet URLs and Discord URLs in .streamlit/secrets.toml.
Portico can send a weekly expense summary to a Discord channel. It reads the
Transactions and Categories tabs, then follows the named transaction sets in
[weekly_summary].watched_transaction_sets. This keeps the notifier aligned
with the Discretionary and other configured dashboard views.
The report includes:
- Current-week categories with activity in the selected transaction sets
- The change from its trailing weekly average
- The largest vendors in each category
- A comparison between the latest group of weeks and the prior group
- Total expenses plus the count and combined absolute amount of all outstanding uncategorized transactions, so refunds do not offset charges
Public defaults use a 48-week average, a four-week comparison, and three
merchants per category. Choose one or more transaction sets and change those
values under [weekly_summary] in config.toml when needed.
- Open Server Settings in Discord.
- Select Integrations, then Webhooks.
- Create a webhook named
Portico. - Select the private channel that will receive the report.
- Copy the webhook URL.
Discord recommends an incoming webhook for a service that only sends messages. Portico does not require a Discord bot or Discord application.
Add only the URL to .streamlit/secrets.toml:
[notifications.discord]
webhook_url = "https://discord.com/api/webhooks/<webhook-id>/<webhook-token>"Choose the report selection in config.toml, not in secrets. Treat the
webhook URL as a password.
The notifier runs inside the same container as the dashboard. Check its configuration, remote spreadsheet access, watched transaction sets, webhook, and timezone:
docker compose exec app python -m src.discord_notifier checkPreview the report without contacting Discord:
docker compose exec app python -m src.discord_notifier previewSend a test message that contains no financial data:
docker compose exec app python -m src.discord_notifier testSend the latest completed weekly report:
docker compose exec app python -m src.discord_notifier sendThe notifier stores sent periods in the portico-state Docker volume. It skips
a period after a successful send.
Discord displays the report as a colored embed. A report with synthetic values looks like this:
Weekly spending
Jul 26 - Aug 1, 2026
π Watched categories
Everyday Food β $120.00 Β· π΄ β² $20.00 above usual
Top vendors: KROGER $80.00 Β· ALDI $40.00
Local Dining β $40.00 Β· π’ βΌ $20.00 below usual
Top vendors: CAFE $40.00
π³ Watched total: $160.00 Β· βͺ β right at usual
π
4-week watched total: $680.00 Β· π’ βΌ $20.00 less than prior 4 weeks
π΅ All expenses: $900.00
π§Ύ Needs categorization: 4 transactions Β· $125.00
Scheduled summaries are disabled by default. Set these values in .env:
TZ=America/Chicago
PORTICO_DISCORD_ENABLED=true
PORTICO_DISCORD_CRON=0 9 * * 0The cron value has five fields: minute, hour, day of month, month, and day of
week. The example sends each Sunday at 9:00 AM in the TZ timezone.
Recreate the Compose service after changing .env. The container log shows the
next scheduled delivery time:
docker compose up --detach --force-recreate
docker compose logs appIf the container is stopped at the scheduled time, that delivery is not run
later. You can send it manually with the docker compose exec command above. Successful
deliveries are recorded, so Portico does not send the same weekly period twice.
The recommended setup is the repository Dev Container. Open the repository in VS Code and choose Dev Containers: Reopen in Container. Then run:
task demoThe container includes the pinned Python, uv, Task, Docker CLI, Buildx, and development dependencies.
The bootstrap installs all tools inside the repository. It does not change the system Python installation.
On Linux:
sh scripts/bootstrap.sh
.tools/bin/task demoOn Windows PowerShell:
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/bootstrap.ps1
.\.tools\bin\task.exe demoYou do not need a system Python, uv, Task, or mise installation.
Contributors who already use uv can run:
uv sync --locked --dev
uv run --locked ruff check .
uv run --locked mypy
uv run --locked pytestSet PORTICO_CONFIG_PATH=portico-demo.toml and run
uv run --locked streamlit run Home.py to start the synthetic demo without
Task. PowerShell uses $env:PORTICO_CONFIG_PATH = "portico-demo.toml".
Before sharing a local demo, run task demo:smoke. It loads every page against
the committed synthetic configuration and catches page-load and import errors
without starting a server.
Task provides short names for the same local checks that CI runs:
.tools/bin/task check
.tools/bin/task container:smokeIn PowerShell, replace .tools/bin/task with .\.tools\bin\task.exe.
Read CONTRIBUTING.md before you submit a change. Community participation follows the Code of Conduct. Read the architecture guide before you design a feature.
Portico is a personal application with no login screen. Source commands and
container ports use 127.0.0.1 by default. Read SECURITY.md before
you expose the app beyond the local computer.
This project is independent. No spreadsheet provider endorses or maintains it.
Copyright 2026 Nick Curry and contributors.
Portico uses the Apache License 2.0.



