User Guide & Privacy Everything you need to know about using Roastfolio and how your data is protected.
Getting Started
Creating an account
- Click Sign Up on the login page and enter your e-mail address and a password (minimum 6 characters).
- Check your inbox for a verification code from Roastfolio and enter it when prompted.
- Choose a display name (nickname) — this is the only personal detail stored beyond your e-mail.
- You are now logged in. Your account and all data are tied to your e-mail address.
Password recovery: If you forget your password, click Forgot password? on the login page. A reset code will be sent to your verified e-mail address.
Creating your first wallet
After logging in, open the Wallets tab. This is your main portfolio management screen. To add a wallet:
- Desktop: click the + Wallet button in the wallet selector panel, type a name, and press Create.
- Mobile: tap the ··· overflow button → + Add wallet, type a name, and press Create.
Name wallets after the brokerage or account they represent (e.g. IKE, IKZE, XTB, Binance). Each wallet tracks one account independently.
Summary is always the first entry and is computed automatically as the aggregate of all your wallets. It is read-only — you cannot add transactions to it directly.
Holdings
Holdings are created automatically when you record a BUY transaction. There is no separate "add holding" form. Each BUY transaction adds to (or creates) the matching position in your wallet.
The holdings table shows each position with live price data, current value, today's change in PLN and %, and allocation percentage. On mobile, holdings display as stacked cards. On desktop, they appear in a table with sortable columns.
To edit or delete a position, tap the ✏️ or 🗑️ icons on the holding row. To record a trade against an existing holding directly, use the Buy or Sell chip buttons on the holding card.
Cash holding: any holding without a ticker (named e.g. Cash or Konto) is treated as a cash position. Its value is used to calculate available cash for BUY transactions.
Recording transactions
Transactions are recorded in a slide-up panel (bottom sheet) that opens without leaving the holdings view. To open it:
- Desktop: click New transaction in the wallet overview bar.
- Mobile: tap the + Transaction floating button at the bottom of the screen.
- From a holding card: tap Buy or Sell to open the panel with that holding pre-selected.
Inside the transaction panel, select the type using the buttons at the top:
| Type | What it does |
|---|---|
| Buy | Adds units to an existing holding or creates a new one. Decreases cash balance. |
| Sell | Removes units from a holding. Increases cash balance. |
| Deposit | Adds cash to the wallet without buying anything. Increases cash balance. Counts toward the monthly contribution streak. |
| Withdrawal | Removes cash from the wallet. Decreases cash balance. Breaks the withdrawal-free streak. |
For Buy and Sell, start by searching for the asset. Two sources are supported:
- Yahoo Finance — for stocks, ETFs, crypto, and international assets (e.g.
CDR.WA,AAPL,BTC-USD). Use arrow keys and Enter to navigate results. - Polish TFI — for Polish mutual funds (TFI). Enter the bankier.pl fund code and press Lookup.
Enter the quantity (number of units) and trade value (total cash impact in PLN). Commission and a free-text comment are optional under Advanced options. A live review summary appears before you submit.
The panel has two tabs — Trade (new transaction) and History (past transactions and daily value snapshots for the selected wallet). On mobile, swipe down from the handle at the top to dismiss the panel.
Auto-add cash: if a BUY transaction would exceed your available cash balance, you can enable Automatically add missing cash before a buy. This records a DEPOSIT for the shortfall automatically before applying the buy.
All-Time High (ATH) management
Roastfolio tracks the All-Time High (ATH) portfolio value for each wallet and for the overall portfolio. ATH is used on the dashboard gauge to show how close you are to your peak. By default, ATH is computed automatically from your daily value snapshots.
To view or edit ATH for a wallet, open the Wallets tab and select the wallet from the list. In the wallet overview bar you will see an All-Time High (ATH) row showing the current peak value, its date, and whether it was set automatically or manually.
Tap the ✏️ icon to expand the ATH editor:
- Manual ATH date — pick the date on which the peak occurred.
- Manual ATH value (PLN) — enter the peak value in PLN.
- Click Save manual ATH to persist your override. The dashboard gauge will immediately reflect the new ATH.
Auto mode: click Auto (reset from history) to discard any manual override and let the system recalculate ATH from your historical daily snapshots. Auto mode is the default for all new wallets.
Manual ATH does not permanently disable automatic updates — if a new daily snapshot exceeds the stored ATH, the system will still update it automatically. Manual ATH only sets the current floor.
Summary wallet — selecting Summary in the wallet list shows and lets you edit the ATH for the aggregated total portfolio value.
Asset Analysis
The Analysis screen provides deep-dive research tools for any stock, ETF, or crypto asset. Use it to explore detailed price history, financial statements, and earnings data before making investment decisions.
Opening the Analysis screen
There are multiple ways to access the Analysis screen:
- From Dashboard movers: Click any holding in the "Daily Movers" section to open its analysis.
- From Portfolio tables: Click any row in the portfolio holdings table (desktop or mobile).
- From Chart legends: Click any company name in pie chart legends.
- From Wallets: Click the company name/logo in any wallet's holdings list.
- Direct search: Open the Analysis tab from the sidebar and search for any ticker symbol.
Quick tip: Cash holdings are not clickable as they have no market data.
Search and selection
At the top of the Analysis screen is a search bar where you can enter any ticker symbol or company name (e.g. AAPL, CDR.WA, Meta, BTC-USD). Results appear instantly from Yahoo Finance's global database. Holdings you already own are marked with a ✓ Owned badge.
Use arrow keys to navigate the dropdown and press Enter to select an asset. The analysis loads automatically with a smooth animated loader.
Price history chart
The main chart shows the asset's price history with the following features:
- Time range buttons: 1D, 1W, 1M, YTD, 1Y, 5Y, ALL — click to reload the chart with different historical depth.
- Transaction markers: If you own the asset, your BUY and SELL transactions appear as markers on the chart:
- 🟢 Green arrow up = BUY transaction
- 🔴 Red arrow down = SELL transaction
- 🟠 Orange circle = both BUY and SELL on the same date
- Tooltip: Hover over a transaction marker to see details (type, quantity, price).
- Responsive zoom: The chart automatically adjusts to fit the selected time period.
Fundamental properties
Below the chart, a grid displays key fundamental metrics fetched from Yahoo Finance:
| Metric | Description |
|---|---|
| Sector & Industry | Business classification (e.g. Technology / Software) |
| Market Cap | Total market capitalization (formatted as B/M/T) |
| P/E Ratio | Trailing and forward price-to-earnings ratios |
| Dividend Yield | Annual dividend as percentage of current price |
| 52-Week High/Low | Peak and bottom prices over the last year |
All metrics display "—" when data is unavailable (common for small-cap stocks, crypto, or newly listed assets).
Cash Flow Statement
For companies with available financial data, the Analysis screen shows a detailed Cash Flow Statement covering the last 5 years. This section includes:
- Annual/Quarterly tabs: Switch between yearly summaries and quarterly breakdowns (last 8 quarters).
- Key metrics: Operating Cash Flow, Free Cash Flow, Capital Expenditures, Dividends Paid, and more.
- Scrollable table: Horizontal scroll on mobile; first column (metric names) stays fixed.
Cash flow data is particularly useful for evaluating a company's ability to generate cash, fund operations, and pay dividends.
Income Statement
The Income Statement section displays profit-and-loss data for the same 5-year period:
- Annual/Quarterly tabs: View yearly income statements or drill into quarterly results.
- Key metrics: Total Revenue, Operating Income, Net Income, EBITDA, Gross Profit, and more.
- Growth analysis: Compare year-over-year or quarter-over-quarter trends directly in the table.
This data helps assess profitability, revenue growth, and operational efficiency over time.
Data availability: Financial statements are pulled from Yahoo Finance. Large-cap stocks typically have complete historical data. Small-cap stocks, ETFs, crypto, and international assets may have limited or missing financial data. If no data is available, the Cash Flow and Income Statement sections will be hidden automatically.
Best practices for analysis
- Before buying: Check the P/E ratio, revenue trends, and cash flow to assess valuation and financial health.
- After earnings: Review the latest quarterly results to see if the company met expectations.
- For dividends: Look at dividend yield and check the cash flow statement to confirm sustainability.
- For growth stocks: Focus on revenue growth and operating cash flow trends rather than current profitability.
- Compare holdings: Open multiple assets (via dashboard or portfolio) to compare fundamentals side-by-side in separate browser tabs.
Privacy & Security
How authentication works
Roastfolio uses AWS Cognito — Amazon's managed identity service — for all authentication. Your password is never stored or even seen by the application code. The login flow uses the industry-standard SRP (Secure Remote Password) protocol: only a cryptographic proof is exchanged, not the plaintext password.
After login, Cognito issues a signed JWT token (JSON Web Token). Every API call includes this token in the Authorization header. The backend Lambda function validates the token's signature against Cognito's public key before processing any request. If the token is invalid, expired, or missing, the request is rejected with HTTP 401.
Token expiry: Access tokens expire after 24 hours. Refresh tokens expire after 30 days. When your session expires you will be redirected to the login page.
Your data — strict per-user isolation
Every item stored in the database (portfolios, holdings, transactions, snapshots) uses your Cognito user ID (sub) as the primary partition key. This is a unique, non-guessable UUID assigned at signup.
The backend code enforces that your token's user ID matches the requested data's partition key on every read and write. It is technically impossible for one logged-in user to access another user's data through the application's API — even if they know the other user's e-mail or ID.
Infrastructure security
Developer access policy
As the developer and sole operator of Roastfolio, I have administrative access to the AWS account that hosts your data. This is technically unavoidable for a solo-operated SaaS. Here is how I limit and account for that access:
| What I have access to | How it is controlled |
|---|---|
| DynamoDB tables (read/write) | IAM — separate dev and prod accounts; no routine prod access |
| CloudWatch logs (Lambda logs) | Logs contain request metadata, not portfolio values |
| AWS CloudTrail | All API calls to prod DynamoDB are audit-logged with timestamp and identity |
| Your password | Never stored anywhere — handled entirely by AWS Cognito |
| Your JWT tokens | Never logged or stored — they expire and are validated in-memory only |
Your data is protected by AWS IAM access controls and all access to the production database is audit-logged via AWS CloudTrail. I do not have routine access to production data. Any access I make for support or maintenance purposes is recorded in the audit log and I commit to not reading, copying, or using your portfolio data for any purpose other than operating the service.
Important transparency note: Roastfolio is not a zero-knowledge application. Because portfolio calculations (live prices, gain/loss, benchmarks) happen on the server, the Lambda function processes your holdings data in plaintext. If you require cryptographic guarantees that no one — including the operator — can ever read your data, a server-side architecture like this cannot provide that. The controls described above are operational and policy-based, not mathematical.
Data encryption
All data is encrypted at rest (DynamoDB server-side encryption, S3 SSE) and in transit (TLS via CloudFront and API Gateway). The encryption keys are AWS-managed (AES-256). As the account owner I have administrative access to these keys, which is consistent with the operational access model described above.
Audit logging
AWS CloudTrail is enabled on the AWS account and records every API call made to DynamoDB, Lambda, and S3, including the identity making the call, the timestamp, and the source IP. These logs are stored in S3 and cannot be altered retroactively without detection. In practice, this means that any access to your data — including by the developer — leaves an immutable record.
Frequently Asked Questions
Can the developer see my portfolio data?
Technically yes — as the AWS account administrator I can query the DynamoDB tables directly. Practically: I do not do this in the normal course of operating the service, and any time I do (e.g. to investigate a bug you reported) it is recorded in CloudTrail. I have committed above not to read or use your data beyond what is necessary to run the service.
If this level of trust is not sufficient for you, I recommend not storing sensitive financial data in any cloud-hosted service without client-side encryption.
How is my data backed up?
All DynamoDB tables have Point-in-Time Recovery (PITR) enabled. This means AWS continuously backs up your data and it can be restored to any point within the last 35 days. In the event of accidental data loss I can restore a table backup to recover your portfolios and transactions.
How do I delete my account?
Go to Settings → Account → Delete account. This will permanently delete your Cognito account, all your portfolios, holdings, transactions, and snapshots from DynamoDB. The deletion is irreversible. Because DynamoDB PITR retains backups for up to 35 days after deletion, residual copies may exist in backups during that window and are then permanently purged.
🏆 Milestones & Achievements
What is this?
Roastfolio tracks your investing habits over time and rewards consistency with streaks, milestones, and badges. The goal is to make the boring middle of long-term investing feel like it's going somewhere — because it is.
Everything is calculated automatically from your transaction history. There is nothing to opt into: the moment you make your first deposit, the system starts tracking. The only things that require manual setup are the monthly deposit goal and retirement plan, which unlock additional streak and milestone types.
Roastfolio's achievement philosophy: badges reward real financial behavior — depositing money, staying invested, avoiding withdrawals — not artificial engagement like logging in every day or watching animations. The tone is sarcastic but the mechanic is sound.
Where to find it
Achievements surface in three places:
- Dashboard engagement strip — a row of scrollable streak pills just below the portfolio header (contribution streak, check-in streak, level, recent badges). Tap any pill to open the full achievements screen.
- Achievements page — accessible from the strip or directly via achievements.html. Shows all streaks with animated ring progress, next milestone progress bar, the full badge collection (unlocked + locked), and a monthly recap.
- Transaction confirmations — after recording a buy, sell, deposit, or withdrawal, a one-liner appears. These are purely informational and slightly sarcastic.
Streaks
A streak is a consecutive run of a specific behavior, measured in months or trading days. Streaks reset if you miss a period — but your personal best is always saved and visible in the achievements screen as a "former streak" record.
| Streak | What it measures | Unit | Resets when | Requires setup? |
|---|---|---|---|---|
| Deposit streak | Consecutive calendar months with at least one DEPOSIT transaction. Buy orders without a new cash deposit do not count. | Months | Any month passes with no deposit recorded (3-day grace into the next month) | No |
| Active days | Total count of distinct calendar days on which any transaction (Buy, Sell, Deposit, Dividend) was recorded. This is a proxy for portfolio engagement — more active portfolio management accumulates faster. | Days | Does not reset — it is a cumulative total, not a consecutive run | No |
| Withdrawal-free streak | Consecutive calendar months without any WITHDRAWAL transaction. Sell orders are not counted — reinvesting proceeds is a valid decision. | Months | Any WITHDRAWAL transaction is recorded | No |
| Goal streak | Consecutive months where total deposits meet or exceed your self-set monthly deposit goal. Changing your goal mid-streak does not reset the counter. | Months | Any month where deposits fall below the goal | Yes — set monthly goal |
| Green months | Consecutive calendar months where your portfolio's market performance is positive — calculated before any new deposits, so it isolates market return from your contributions. | Months | Any month ends with a negative market return | No |
| Beat benchmark [planned] | Consecutive months where your portfolio's total return exceeds your chosen benchmark (WIG20, MSCI World, S&P 500). Requires benchmark data integration — not yet available in the current build. | Months | Any month your return trails the benchmark | Yes — enable benchmark |
Why only DEPOSIT for the contribution streak, not BUY? Buying with cash that's already in your portfolio is not new capital entering the system. The streak is designed to reward the habit of adding fresh money each month. Moving existing cash from one holding to another doesn't count.
Milestones
Milestones are one-time achievements. When you cross one, a badge unlocks and a roast fires. They never expire and can only be earned once each. The Achievements screen shows four separate milestone timelines — Portfolio value, Investor journey, Monthly deposit best, and FIRE progress (if a retirement plan is configured). Each timeline shows past milestones with the date reached, the next target with a live progress bar, and future milestones muted below.
Portfolio value
Triggered when your total portfolio value (all portfolios combined) crosses a threshold for the first time. The badge is awarded on first crossing only — if the market dips below and recovers, no second badge. XP from each threshold is awarded cumulatively: a 150 000 PLN portfolio earns XP for all thresholds below it.
| Value (PLN) | Roastfolio says… |
|---|---|
| 1 000 | "It begins. Technically you're an investor now." |
| 10 000 | "Five figures. A threshold that matters." |
| 50 000 | "Halfway to six figures. The market will try to undo this. Don't let it." |
| 100 000 | "Six figures. This is the number that changes behavior. Guard it." |
| 250 000 | "A quarter million. At this point your money has a support group." |
| 500 000 | "Half a million. The math is doing most of the work now." |
| 1 000 000 | "One million. You either DCA'd for 30 years or got very lucky." |
| 2 000 000 | "Two million. The returns alone are someone's annual salary." |
| 5 000 000 | "Five million. The portfolio manages you more than you manage it." |
Investor journey (from first investment date)
Time-based milestones counted from the date of your first ever recorded transaction — not account creation. The clock starts when money moves.
| Since first investment | Milestone |
|---|---|
| 3 months | "Still here. Initial excitement survived." |
| 6 months | "Half a year. One market wobble behind you, probably." |
| 1 year | "One full year as an investor. All four seasons of market behavior." |
| 2 years | "The 'long term' is no longer hypothetical." |
| 5 years | "You're a different investor than when you started." |
| 10 years | "A decade. Compounding has had time to become your co-pilot." |
Monthly deposit personal best
Unlocked the first time you deposit above a threshold in a single calendar month — rewards your biggest months, not just consistency.
Thresholds: 1 000 / 2 500 / 5 000 / 10 000 / 25 000 / 50 000 / 100 000 PLN in one month.
Retirement progress
Triggered at 10 / 25 / 50 / 75 / 90 / 100% of your FIRE target. Only active if you have a retirement plan configured. See Setup & configuration for details.
Badges & tiers
Every milestone and streak achievement unlocks a badge. Badges come in five tiers — rarity reflects how long or how difficult the achievement is to earn, not whether it's valuable. A Seed badge for your first deposit matters.
| Tier | What it represents | Visual |
|---|---|---|
| Seed | Participation and first steps. Nearly everyone earns these early. | Gray border, muted glow |
| Bronze | Early consistency — showing up for a few months, hitting initial thresholds. | Amber border |
| Silver | Sustained effort — a full year of something, meaningful portfolio thresholds. | Steel blue border |
| Gold | Genuine milestones — six-figure portfolio, multi-year streaks, survival through bad markets. | Yellow border, slow pulse glow |
| Diamond | Rare, long-term, legendary. Five-year streaks, seven-figure portfolio, full trading year of check-ins. | Cyan border, continuous shimmer pulse |
In the achievements screen, locked badges appear as silhouettes at reduced opacity. Tapping a locked badge shows exactly what it requires and how close you are. Nothing is hidden — you always know what's next.
Retroactive awards: When the achievements system launches for your account, all badges you've already earned through past transactions are awarded immediately and silently. You'll see a "New" indicator in the achievements drawer and a one-time message: "We went through your history. Turns out you've been building this longer than you realized."
XP & Level Calculation
XP (experience points) are derived entirely from real transaction and portfolio snapshot data. There are no artificial engagement loops — you earn XP for financially meaningful actions only. The engine (xp-engine.js) recomputes automatically every time transaction history or portfolio prices are refreshed.
No manual entry. XP is never entered by hand. If a transaction is deleted, the XP it generated disappears on the next recalculation. The number is always consistent with your actual history.
Storage decision — why XP is not stored in the database. XP, levels, badges, milestones, and streaks are computed client-side on every load from transaction history already fetched from the API. Storing computed values in DynamoDB would create a stale-data sync problem every time calculation rules change. Persisted between sessions: streak personal bests (localStorage — they only ever increase), monthly deposit goal (localStorage key xpe_deposit_goal), and FIRE target (localStorage key xpe_fire_target, refreshed from the retirement plans API). Because computation is deterministic and fast (<10ms), no server-side caching is needed. Existing and new users get a full retroactive recalculation from their complete transaction history automatically on every load.
XP rule table
| Category | Action | XP awarded |
|---|---|---|
| Deposits | Per deposit event | +15 |
| Amount ≥ 500 PLN | +5 bonus | |
| Amount ≥ 1 000 PLN | +15 bonus | |
| Amount ≥ 5 000 PLN | +30 bonus | |
| Amount ≥ 10 000 PLN | +60 bonus | |
| Consistency | Per calendar month with ≥1 deposit | +20 |
| Goal | Per month with total deposits ≥ 1 000 PLN | +10 bonus |
| Income | Per dividend received | +15 |
| Trading | First sell transaction | +20 |
| Each additional sell | +5 | |
| Portfolio milestones | Portfolio ≥ 1 000 PLN | +50 |
| Portfolio ≥ 10 000 PLN | +100 | |
| Portfolio ≥ 50 000 PLN | +200 | |
| Portfolio ≥ 100 000 PLN | +400 | |
| Portfolio ≥ 250 000 PLN | +600 | |
| Portfolio ≥ 500 000 PLN | +1 000 | |
| Diversification | 3+ unique holdings bought | +30 |
| 5+ unique holdings bought | +50 | |
| 10+ unique holdings bought | +80 | |
| Retirement | Retirement plan configured | +50 |
| Badges | Per badge unlocked | +50 |
Deposit bonuses are tiered, not cumulative. A 1 200 PLN deposit earns base (+15) plus the ≥1 000 PLN tier (+15) — the +5 (≥500) tier is superseded. Diversification bonuses are cumulative: reaching 10 holdings awards all three tiers (+30 + +50 + +80 = +160 total). Portfolio milestone bonuses are also cumulative — a 150 000 PLN portfolio earns +50 + +100 + +200 + +400 = 750 XP for all four crossed thresholds.
Level thresholds
| Level name | XP required (total) |
|---|---|
| Seed | 0 |
| Observer | 100 |
| Participant | 250 |
| Saver | 500 |
| Contributor | 1 000 |
| Operator | 2 500 |
| Veteran | 5 000 |
| Institution | 10 000 |
Streak calculation
Streaks are computed from transaction history every time data refreshes. Each streak also persists its personal best in localStorage so the all-time high is never lost if a streak resets.
| Streak | How it’s calculated |
|---|---|
| Deposit streak | Consecutive calendar months (YYYY-MM) ending at the most recent month that contain at least one deposit. A month with zero deposits breaks the streak. |
| Active days | Total count of distinct calendar days on which any transaction (buy, sell, deposit, dividend) occurred. Used as a proxy for engagement frequency. |
| Withdrawal-free | Number of calendar months between the date after the last withdrawal and today. If no withdrawal has ever been recorded, the streak counts from the first deposit date. |
| Goal streak | Consecutive calendar months (most recent first) where the sum of all deposits equals or exceeds the monthly goal (default: 1 000 PLN across all portfolios). |
| Green months | Consecutive calendar months where the net portfolio return — (end value − start value) − (new capital deposited) — is positive. Requires portfolio snapshot history. |
Badge derivation
Each badge is evaluated against a specific condition derived from real data. Below is the full derivation logic for every badge in the system.
| Badge | Unlocks when… |
|---|---|
| Showing Up | At least one deposit exists in transaction history |
| Reliable | Deposit months total ≥ 3 (deposit streak or cumulative) |
| Eyes Open | Distinct transaction-active days ≥ 7 |
| Hands Off | Withdrawal-free streak ≥ 6 months and no withdrawal ever recorded |
| Four Digits | Historical peak portfolio value ≥ 1 000 PLN |
| Getting Serious | Historical peak portfolio value ≥ 10 000 PLN |
| Not One Basket | Unique tickers bought ≥ 3 |
| Actually Diversified | Unique tickers bought ≥ 5 |
| Baptism by Fire | Portfolio saw a ≥10% drawdown from running peak and no withdrawal was ever recorded |
| FIRE Curious | At least one retirement plan saved (checked via RetirementPlansClient, cached in localStorage) |
| Red Day Survivor | Portfolio snapshot history shows a ≥10% drawdown, or snapshot history exists (proxy: you have seen red days) |
| Sold Something | At least one Sell transaction exists |
| Committed | Goal streak ≥ 3 consecutive months |
| The Long Game | Deposit months ≥ 12 |
| Weekly Habit | Distinct active days ≥ 90 |
| Untouched | Withdrawal-free streak ≥ 36 months |
| On Track | Goal streak ≥ 12 months |
| Six Figures | Historical peak portfolio value ≥ 100 000 PLN |
| Held the Line | Portfolio saw a ≥20% drawdown and no withdrawal ever recorded |
| World Citizen | Transactions span ≥3 country/asset-class proxies (Polish .WA tickers, foreign tickers, crypto) |
| Tax Efficient | Transactions exist in both an IKE and an IKZE wallet |
| Institutionalized | Deposit months ≥ 36 |
| Comma Club | Historical peak portfolio value ≥ 1 000 000 PLN |
Data required: Most badges only need transaction history. Green months, drawdown badges (Baptism by Fire, Held the Line, Red Day Survivor), and portfolio milestone badges additionally require portfolio snapshot data — available once the app has loaded at least one price update.
Most streaks and milestones require no setup — they're computed automatically. Two features need configuration to unlock their respective achievements:
XPEngine.setDepositGoal(amount) in the browser console, or set localStorage.xpe_deposit_goalSet a target amount (in PLN) you want to deposit each month. This activates the Goal streak counter and the monthly goal bonus XP. The goal applies to your total deposits across all portfolios in a calendar month — not per individual portfolio. Default is 1 000 PLN.
Changing your goal at any time does not reset your streak. The system records that you had a goal and hit it; what the goal was is secondary to whether you met it.
Set a target portfolio value you want to reach before retirement. This activates the retirement progress milestones (10% / 25% / 50% / 75% / 90% / 100%) and the corresponding badges. The FIRE progress is measured as: total current value of all portfolios divided by your retirement target.
IKE and IKZE utilization tracking is independent of your FIRE target — it looks at deposits into accounts tagged as IKE/IKZE and compares them to the annual statutory limits (updated each year).
Multiple retirement plans — how progress is calculated
Roastfolio allows you to create more than one retirement plan (e.g., a conservative scenario and an aggressive scenario with different target values). Here is how the achievements system handles that:
One plan drives the achievement badges at a time. If you have multiple retirement plans, you designate one as your primary plan for achievements. Only this plan contributes to the FIRE progress milestones and retirement badges.
If no primary is designated, the system defaults to the most recently modified plan.
| Scenario | How it's handled |
|---|---|
| Single retirement plan | Used automatically. No action needed. |
| Multiple plans, one marked primary | The primary plan drives all retirement badges and the FIRE progress bar. |
| Multiple plans, none marked primary | The most recently modified plan is used. A soft prompt suggests marking one as primary. |
| IKE / IKZE utilization badges | Always calculated from actual deposits into IKE- and IKZE-tagged accounts — independent of which retirement plan is primary. The annual limit comparison is against the statutory maximum regardless of your personal FIRE target. |
The FIRE progress percentage is always calculated as:
/* Total value of ALL portfolios ÷ primary retirement plan target */ FIRE % = (sum of all portfolio values) / (primary plan target) × 100
This means your full wealth counts toward the goal, not just the assets in "retirement-tagged" portfolios. Rationale: when you retire, all your money is relevant — artificially restricting the calculation to specific portfolios would understate your actual progress.
Monthly deposit goal — similar rule applies: The goal streak measures total deposits across all portfolios in a calendar month. If you set a goal of 1 000 PLN/month and deposit 400 PLN into IKE and 700 PLN into XTB, the streak counts it as 1 100 PLN — goal met. The system doesn't care which portfolio received the money.
🎨 Design System
This section documents the design decisions that define Roastfolio's visual identity. It is updated whenever a significant UI change is made so the reasoning behind each decision is never lost.
Audience: This section is for ADVANCED users and contributors who want to understand why the app looks and behaves the way it does — not just what it looks like.
Design Philosophy
Roastfolio sits at the intersection of premium fintech and internet humor. Every design decision asks two questions: does this feel like it belongs next to Revolut? and does this feel like it was made by someone who lost money on CDR and laughed about it?
The design language is dark-native (not dark-mode as an afterthought), card-first, and number-obsessed — financial data is the hero, personality is the seasoning.
The tone lives in copy, not chrome. Roastfolio's sarcasm comes from error messages, milestone notifications, and empty states. The UI itself is as premium as Revolut. Users need to trust the app with financial data — humor and visual roughness don't mix well in fintech.
Design inspiration: Revolut · Robinhood · Coinbase · TradingView · Apple Fitness · Linear
Color Palette
Background depth layers
Brand accent
This specific blue — not Robinhood green, not Coinbase deep blue — lands in "trustworthy retail bank that's cooler than your actual bank." Saturated enough to feel premium, desaturated enough to not feel crypto-scammy. Green is avoided as a brand accent because it is semantically loaded in finance (it means profit).
Semantic colors
Text hierarchy
Typography
System font stack — ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif. On iPhone, this resolves to SF Pro; on macOS, SF Pro or Helvetica Neue; on Android, Roboto; on Windows, Segoe UI. Zero network cost, perfect rendering, device-tuned letter spacing.
-webkit-font-smoothing: antialiased and text-rendering: optimizeLegibility are applied globally so text appears sharp on retina and ProMotion displays. font-variant-numeric: tabular-nums is applied to all financial figures — keeping column alignment stable and preventing number-jank during live value updates.
Spacing & Grid
4px base unit — matches the iOS and Android native layout grid. Every spacing value is a multiple of 4, which means composed layouts snap to device pixels and feel native rather than web-ported. At 3× pixel density (iPhone 15 Pro), a 4px CSS unit is 12 physical pixels — sharper-than-retina rendering for borders and shadows.
Card System
Cards are the primary content container. No raw tables on mobile, no naked lists. Each card uses border-radius: 16px (the Revolut/Apple standard for modern cards), a 1px semi-transparent blue-tinted border, and an inset top-shadow that simulates light catching the upper rim — borrowed from Apple's glass design language.
#0e1a2a background · 1px border at 12% opacity · inset 0 1px 0 rgba(255,255,255,0.04) top edge highlight#132032 + drop shadow · used for overlays and action panelsscale(0.992) on tap — registers as "responded" without feeling like a button press. Same micro-interaction as Apple app icons.Button System
| Variant | Usage rule | Key traits |
|---|---|---|
| Primary | One per screen. High-value action (Sign in, Save, Confirm trade). | Gradient accent → accent-strong + colored box-shadow. 44px height. Scale 0.97 on press. |
| Secondary | Supporting action alongside primary. | Ghost with 1px border. 44px height. |
| Ghost | Tertiary or destructive context. | No border, no background. Danger variant turns red. |
| FAB | One per page. Always-visible primary mobile action (New Transaction). | Fixed position above tab bar. bottom: calc(env(safe-area-inset-bottom) + 72px). Pill shape. Accent gradient + glow shadow. |
| Buy / Sell chips | Inline on holding cards. | 32px height. Green/red tinted background + border. 12px bold text. |
Why gradient on primary? A flat background on a primary button reads as a toggle. The gradient + shadow creates visual mass that communicates "this is the action" without relying on color alone.
Bottom Sheets & Modals
Bottom sheets are the correct modal pattern for mobile. They emerge from the direction of the thumb, they don't block the full screen (the user still knows where they are), they match iOS/Android native sheet behavior exactly, and they're trivially dismissible (tap backdrop or swipe down).
dvh (dynamic viewport height) instead of vh fixes the iOS Safari address-bar bug where 100vh includes hidden browser chrome.cubic-bezier(0.32, 0.72, 0, 1) — Apple's spring-curve approximation for sheet presentations. Creates natural deceleration that reads as physical.backdrop-filter: blur(4px) on the overlay so the user retains spatial context. Semi-transparent dark overlay at 82% opacity.env(safe-area-inset-bottom) so content isn't hidden under iPhone home indicator.Responsive Breakpoints
Mobile-first: no media query = mobile base. There is no classical 768px tablet breakpoint. The app jumps from phone (≤640px) to desktop (≥641px). Tablet-specific layouts add code for an audience that doesn't exist in personal finance apps — retail investors use iPhones and laptops, not iPads for portfolio tracking.
CSS Design Tokens
All design values live as CSS custom properties on :root. Component CSS never uses raw values — always tokens. This means a brand color change is a one-line edit, not a search-and-replace.
/* ── Background depth ── */ --fintech-bg: #090f18; --fintech-surface: #0e1a2a; --fintech-surface-raised: #132032; --fintech-surface-strong: #1a2c42; /* ── Brand ── */ --fintech-accent: #2b88cf; --fintech-accent-strong: #36a3f0; /* ── Semantic ── */ --fintech-success: #27ae60; --fintech-danger: #c0392b; --fintech-warning: #e67e22; /* ── Typography ── */ --fintech-text: #e8f0f8; --fintech-text-muted: #6a8ba8; --fintech-text-subtle: #7fa4c4; /* ── Shape ── */ --fintech-radius-sm: 8px; --fintech-radius-md: 12px; --fintech-radius-lg: 16px; /* cards */ --fintech-radius-xl: 20px; /* bottom sheets */ /* ── Shadow ── */ --fintech-shadow-sm: 0 1px 4px rgba(0,0,0,0.3); --fintech-shadow-md: 0 4px 16px rgba(0,0,0,0.35); --fintech-shadow-accent: 0 8px 24px rgba(43,136,207,0.4); --fintech-shadow-inset: inset 0 1px 0 rgba(255,255,255,0.04); /* ── Borders ── */ --fintech-border: rgba(74,159,212,0.12); --fintech-border-strong: rgba(74,159,212,0.22); /* ── Spacing ── */ --fintech-space-1: 4px; --fintech-space-2: 8px; --fintech-space-3: 12px; --fintech-space-4: 16px; --fintech-space-6: 24px; --fintech-space-8: 32px;
Motion & Animation
Motion communicates confidence. Physical-feeling animations tell the user the UI is responsive and stable. CSS default easing feels like something broke. iOS-native easing feels like money is being handled correctly.
cubic-bezier(0.32,0.72,0,1)cubic-bezier(0.2,0,0,1)120ms200ms300ms0.992:active. Under 1% scale reduction — registers as "responded" without feeling like a button.Never animate layout properties (width, height, top, left, margin). Animate only transform, opacity, and filter. Layout animations trigger reflow and cause jank on mobile — especially visible during scroll on iPhones with ProMotion displays.
Mobile UX Quality Checklist
A reference of intentional decisions that separate native-feel PWAs from desktop-ports-on-mobile. Every item below is implemented in the current build.
| Category | Decision | Why it matters |
|---|---|---|
| Font rendering | -webkit-font-smoothing: antialiased + text-rendering: optimizeLegibility on body | Text looks razor-sharp on retina and ProMotion screens. The difference is visible even to non-designers. |
| Font stack | ui-sans-serif, system-ui, -apple-system — full system chain | SF Pro on iOS, Roboto on Android, Segoe UI on Windows. Zero loading cost, native letter-spacing and ligatures per device. |
| Responsive title | clamp(1.4rem, 5.5vw, 2rem) on the brand title | Prevents the header from crowding on 320px iPhone SE and smaller Android devices. |
| Tab navigation | SVG icons instead of emoji in the bottom tab bar | Emoji renders inconsistently across Android, Windows, and older iOS. SVG icons are crisp at any DPI, visually consistent, and brand-controlled. |
| Touch targets | Bottom tab bar buttons fill the full width ÷ 7; FAB is 48×48px minimum | Apple HIG minimum: 44×44pt. Smaller targets cause mis-taps on moving transit. |
| Viewport zoom | viewport-fit=cover without maximum-scale=1.0 | Removing maximum-scale=1.0 restores accessibility zoom for users with low vision. Safari since iOS 10 handles input zoom prevention natively. |
| Card radius | Dashboard cards use border-radius: 18px, reducing to 14px on mobile | Matches iOS native card radius (introduced in iOS 14). Radiused corners read as modern, safe, and touchable. |
| Card shadows | 0 4px 24px rgba(31,95,159,0.10), 0 1px 4px rgba(31,95,159,0.06) | Dual-layer shadow creates depth. The tight inner shadow catches ambient light; the wide outer shadow grounds the card. Matches Revolut and Apple Card UI. |
| Safe-area insets | env(safe-area-inset-*) on header padding, FAB bottom, tab bar bottom | Prevents content from sliding under the Dynamic Island, notch, or home indicator on all iPhone generations from X onward. |
| Overlay scroll | overscroll-behavior: contain on overlay panels | Prevents bottom-sheet scroll from rubber-banding the page behind it on iOS Safari. Eliminates one of the most jarring mobile web artefacts. |
| Tap highlight | -webkit-tap-highlight-color: transparent on all interactive elements | Removes the blue flash on tap. iOS apps don't have it; web apps shouldn't either. |
| Touch action | touch-action: manipulation on all buttons | Eliminates 300ms tap delay on touch devices without disabling scroll. Buttons respond the instant the finger lifts. |
| Inline styles | Transactions tab layout extracted to .tx-summary-bar, .tx-activity-card, .tx-history-card | Inline styles skip the design-token system and dark-mode overrides. Semantic class names allow responsive and theme overrides. |
| Tab icons | Statistics headings use plain text, not emoji prefixes | Emoji in headings breaks screen readers, renders inconsistently between platforms, and undermines the premium brand tone. |
Ongoing rule: Every new section of UI must pass the question: does this feel like a native iOS finance app, or like a website that happens to be on a phone? If the answer is the latter, the section is not finished.
Gamification UX Audit — Applied Fixes
A senior UX audit was applied across the achievements, streaks, milestones, and engagement strip pages. The following issues were identified and fixed.
| Finding | Fix applied | Rule |
|---|---|---|
| Viewport zoom blocked on achievements page | Removed maximum-scale=1.0 from achievements.html viewport meta | Accessibility — users with low vision must be able to zoom. Apply to all pages, not just index. |
| Back button was 36×36px | .ach-back-btn enlarged to 44×44px, added touch-action: manipulation + :active state | Apple HIG: minimum 44pt touch target on all interactive controls. |
| Filter tabs height ~30px | .ach-filter-btn padding changed to 9px 14px, min-height: 40px added, added inset box-shadow on active state | Segmented control buttons must be ≥40px tall on mobile. The active state should be visually distinct beyond background opacity alone. |
| Badge press scale too aggressive (0.93) | Changed .ach-badge:active { transform: scale(0.97) }. Hover only activates on hover: hover devices. | Industry standard for card press-feedback is 0.96–0.97. 0.93 feels like a crush, not a tap. Desktop-only hover effects must not fire on touch devices (@media (hover: hover)). |
| Locked badges opacity 0.3 — nearly invisible | Increased to opacity: 0.45; filter: grayscale(0.6) | Users should be able to see and identify locked badges so they have something to aim for. Apple Health and Strava use 0.4–0.5 opacity for locked achievements. |
| Badge names 10.5px, tier labels 9px | Badge names → 11px, tier labels → 10px | 11px is the minimum comfortable legibility on iPhone at arm's length. Font sizes should be on the 4px design grid (8, 10, 11, 12, 13, 14, 15, 16…). |
| Badge progress bar border-radius: 0 | Changed track + fill to border-radius: 3px, height increased from 3px to 4px | Every other progress bar in the codebase has rounded ends. An angular progress bar is a visual inconsistency that breaks the premium feel. 4px height is the minimum thumb-visible size. |
| Streak card hover causes layout shift on touch | Removed transform: translateY(-2px) from hover. Moved hover rule inside @media (hover: hover). Reduced min-width from 122px to 118px. | CSS hover states fire as ghost-hover on iOS Safari after a tap, causing the card to jump. Touch-safe hover requires the hover: hover media query guard. |
| Streak scroll has no right-edge affordance | Added mask-image fade on the right edge of .ach-streaks-scroll. Added overscroll-behavior-x: contain. | Horizontal scrollers must have a visual indicator that more content exists (partial card peek or gradient fade). Without it, users assume all cards are visible. |
| Milestone roast hidden on small screens | Changed from display: none to -webkit-line-clamp: 2; text-align: left at 520px | The sarcastic roast is Roastfolio's core personality differentiator. Hiding it entirely on the most common screen size (375px iPhone) removes the key engagement hook. |
| achFadeUp translateY(18px) too dramatic for inline cards | Reduced to translateY(10px) | Full-page hero entrances use 16–24px. Inline card entrances should use 8–12px. Over-travel reads as heavy and slow on mobile. |
| Infinite animations drain battery on mobile | Added @media (prefers-reduced-motion: reduce) block disabling achDiamondPulse, achGoldShimmer, achNodePulse and all entrance transitions | Continuous animations prevent the GPU from entering low-power mode on OLED devices. System preference must always be respected per WCAG 2.3. |
| aria-atomic="true" on badge grid | Removed aria-atomic="true" from #ach-badges-grid | With atomic=true, every filter change would read the entire badge grid (22+ items) to screen reader users. Without it, only the changed nodes are announced. |
| Engagement strip horizontal scroll bleeds into page scroll on iOS | Added overscroll-behavior-x: contain to .eng-strip. Reduced animation from 0.28s to 0.22s. | Without overscroll containment, reaching the end of a horizontal strip on iOS triggers page rubber-band scroll, breaking the sense of two separate scroll axes. |
| Milestone timeline connector barely visible | Increased connector opacity from 0.18 → 0.25 (active) / 0.07 → 0.10 (faded) | The timeline metaphor depends on the connector line being clearly readable. 0.18 opacity on a dark surface is below perceptible threshold on many screens. |