* [AI] Add secure custom font support for custom themes
Implement safe font-family references in custom themes via CSS variables
(--font-body, --font-mono, --font-heading, etc.) validated against a
curated allowlist of system-installed and web-safe fonts.
Security approach: Only fonts already present on the user's OS or bundled
with the app are allowed. No @font-face, no url(), no external font
loading — this prevents third-party tracking via font requests while
still enabling meaningful font customization in themes.
Key changes:
- Add SAFE_FONT_FAMILIES allowlist (~80 fonts: generic families, bundled
fonts, and common system fonts across platforms)
- Add validateFontFamilyValue() for comma-separated font stack validation
- Route --font-{body,mono,heading,family,ui,display,code} properties
through the font validator instead of the color validator
- Update index.html to use var(--font-body, ...) with current Inter
Variable stack as fallback
- Add comprehensive tests for valid/invalid font values and security
edge cases (url injection, javascript:, expression(), etc.)
https://claude.ai/code/session_01D4ASLpcBCvWF1nzPLz9Tw5
* [AI] Add @font-face support with data: URI embedding for custom themes
Enable truly custom fonts in themes while maintaining zero runtime network
requests. Theme authors can include font files in their GitHub repos, and
fonts are automatically downloaded and embedded as data: URIs at install
time — the same approach used for theme CSS itself.
Security model:
- @font-face blocks only allow data: URIs (no http/https/relative URLs)
- Font MIME types are validated (font/woff2, font/ttf, etc.)
- Individual font files capped at 2MB, total at 10MB
- @font-face properties are allowlisted (font-family, src, font-weight,
font-style, font-display, font-stretch, unicode-range only)
- Font-family names from @font-face are available in --font-* variables
- No runtime network requests — all fonts stored locally after install
Key additions:
- extractFontFaceBlocks(): parse @font-face from theme CSS
- validateFontFaceBlock(): validate properties and data: URIs
- splitDeclarations(): semicolon-aware parser that respects data: URIs
- embedThemeFonts(): fetch font files from GitHub, convert to data: URIs
- ThemeInstaller calls embedThemeFonts() during catalog theme installation
- 30+ new test cases for @font-face validation and security edge cases
Example theme CSS with custom fonts:
@font-face {
font-family: 'My Font';
src: url('./MyFont.woff2') format('woff2');
}
:root { --font-body: 'My Font', sans-serif; }
https://claude.ai/code/session_01D4ASLpcBCvWF1nzPLz9Tw5
* [AI] Rename --font-body CSS variable to --font-family
https://claude.ai/code/session_01D4ASLpcBCvWF1nzPLz9Tw5
* [AI] Remove font-family allowlist and broaden --font-* regex
- Remove SAFE_FONT_FAMILIES allowlist and SAFE_FONT_FAMILIES_LOWER lookup.
Any font name is now valid in --font-* properties. Referencing a font
that isn't installed simply triggers the browser's normal fallback — no
network requests, no security risk. Function calls (url(), expression(),
etc.) are still blocked.
- Change the --font-* property regex from a specific list
(family|mono|heading|...) to match all --font-* variables, so theme
authors can use any --font-prefixed custom property.
https://claude.ai/code/session_01D4ASLpcBCvWF1nzPLz9Tw5
* [AI] Simplify and improve custom font validation code
Code quality improvements from review:
- Remove dead `declaredFonts` Set (was populated but never read after
allowlist removal)
- Extract `stripQuotes()` helper to deduplicate quote-stripping logic
between `validateFontFamilyValue` and `validateFontFaceBlock`
- Replace confusing `const searchFrom = 0` loop with `for (;;)` idiom
in `extractFontFaceBlocks`
- Use index tracking (`content.substring(start, i)`) instead of
character-by-character string concatenation in `splitDeclarations`
- Use `splitDeclarations` in `validateRootContent` instead of naive
`split(';')` for consistency and correctness
- Parallelize font fetches in `embedThemeFonts` with `Promise.all`
instead of sequential awaits
- Replace byte-by-byte base64 conversion with chunked
`arrayBufferToBase64()` helper (8KB chunks)
- Reuse indexOf-based @font-face parsing in `embedThemeFonts` instead
of fragile `[^}]*` regex that can't handle large data URIs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Enhance font validation in customThemes.ts
* Add custom release notes for upcoming feature: support for custom fonts in themes
* [AI] Simplify @font-face validation to only block external URLs
Remove ~210 lines of overly thorough font validation (MIME type allowlists,
base64 encoding checks, format hint validation, @font-face property allowlists,
font-family name regex) and replace with a single function that enforces the
actual security goal: rejecting non-data: URIs to prevent external resource
loading. Size limits for DoS prevention are preserved.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update Content Security Policy to include font-src directive
Enhance the Content Security Policy in both the desktop client and sync server to allow font loading from data URIs. This change ensures that custom fonts can be embedded securely while maintaining the existing security measures for other resources.
* Enhance font-family validation to disallow empty values
Update the `validateFontFamilyValue` function to throw an error for empty font-family values, improving security and validation accuracy. Adjust tests to reflect this change, ensuring that empty values are properly handled as invalid.
* Enhance validation for CSS custom properties in customThemes.ts
Add comprehensive checks in the `validateRootContent` function to ensure CSS custom properties start with '--', contain valid characters, and do not end with a dash. This improves error handling for invalid property names, ensuring better compliance with CSS standards.
* [AI] Fix path traversal, spaces in font URLs, and add embedThemeFonts tests
Reject path-traversal (../) and root-anchored (/) font paths in
embedThemeFonts to prevent URL manipulation. Fix URL regex to handle
quoted filenames with spaces (e.g. "Inter Variable.woff2"). Add unit
tests covering both security validations and normal embedding flow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Implement font size budget enforcement in embedThemeFonts function
* Add global unstubbing in afterEach for embedThemeFonts tests
---------
Co-authored-by: Claude <noreply@anthropic.com>
This is the main project to run Actual, a local-first personal finance tool. It comes with the latest version of Actual, and a server to persist changes and make data available across all devices.
Getting Started
Actual is a local-first personal finance tool. It is 100% free and open-source, written in NodeJS, it has a synchronization element so that all your changes can move between devices without any heavy lifting.
If you are interested in contributing, or want to know how development works, see our contributing document we would love to have you.
Want to say thanks? Click the ⭐ at the top of the page.
Using the CLI tool
Node.js v22 or higher is required for the @actual-app/sync-server npm package
Install globally with npm:
npm install --location=global @actual-app/sync-server
After installing, you can execute actual-server commands directly in your terminal.
Usage
actual-server [options]
Available options
| Command | Description |
|---|---|
-h or --help |
Print this list and exit. |
-v or --version |
Print this version and exit. |
--config |
Path to the config file. |
--reset-password |
Reset your password |
Examples
Run with default configuration
actual-server
Run with custom configuration
actual-server --config ./config.json
Reset your password
actual-server --reset-password
Documentation
We have a wide range of documentation on how to use Actual. This is all available in our Community Documentation, including topics on installing, Budgeting, Account Management, Tips & Tricks and some documentation for developers.
Feature Requests
Current feature requests can be seen here. Vote for your favorite requests by reacting 👍 to the top comment of the request.
To add new feature requests, open a new Issue of the "Feature Request" type.