This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Nextcloud Budget is a comprehensive financial management app for Nextcloud. It tracks spending, manages multiple accounts, forecasts balances, and provides budgeting tools. The app follows Nextcloud's standard app architecture with a PHP backend and JavaScript frontend.
Tech Stack:
- Backend: PHP 8.1+, Nextcloud App Framework
- Frontend: JavaScript (ES6+), Chart.js for visualizations
- Build: Webpack with Babel, npm scripts
- Database: MySQL/MariaDB, PostgreSQL, or SQLite
- Dependencies: Composer (PHP), npm (JavaScript), TCPDF (PDF generation)
# Development build with source maps
npm run dev
# Watch mode - auto-rebuild on changes
npm run watch
# Production build (minified, optimized)
npm run build
# Lint JavaScript
npm run lint
# Auto-fix linting issues
npm run lint:fix# Install PHP dependencies (production)
composer install --no-dev --optimize-autoloader
# Install all dependencies including dev tools
composer install
# Lint PHP files
composer run lint
# Run PHPUnit tests
composer run test:unit
# Run Psalm static analysis
composer run psalmThe Makefile provides convenient shortcuts:
# Development
make dev # Build for development
make watch # Watch and rebuild on changes
make composer-dev # Install all composer dependencies
# Testing & Quality
make lint # Run all linters (PHP + JS)
make lint-fix # Auto-fix linting issues
make test # Run PHPUnit tests
make psalm # Run static analysis
# Building
make build # Build production package
make appstore # Create signed app store tarball
make clean # Remove build artifacts
# Nextcloud Integration (run from budget/ directory in Nextcloud apps/)
make enable # Enable app: php ../../occ app:enable budget
make disable # Disable app: php ../../occ app:disable budget
make migrate # Run database migrations: php ../../occ migrations:execute budget# Run all unit tests
make test
# OR
cd budget && ../vendor/bin/phpunit -c tests/phpunit.xml
# Run specific test file
../vendor/bin/phpunit tests/Unit/Service/TagSetServiceTest.php
# Run specific test method
../vendor/bin/phpunit --filter testCreateTagSet tests/Unit/Service/TagSetServiceTest.phpThe Nextcloud app store requires two separate signatures:
- Internal signature.json - Created by
php occ integrity:sign-app, included in tarball for post-installation integrity verification - Tarball signature - Created by
openssl dgst, provided to app store during submission for download validation
Prerequisites:
- Official Nextcloud code signing certificate and private key in
~/.nextcloud/certificates/ - Docker container with Nextcloud instance running (for
occcommand) - Certificates:
budget.key(private key) andbudget.crt(Nextcloud-signed certificate) - Container ID:
docker ps | grep nextcloudto find your container
Complete Build Process:
# 0. Ensure version is committed and tagged
cd budget
# Update version in appinfo/info.xml
# Update CHANGELOG.md
git add appinfo/info.xml CHANGELOG.md
git commit -m "chore: Bump version to X.Y.Z"
git tag vX.Y.Z
git push && git push --tags
# 1. Clone fresh from git tag IN CONTAINER
docker exec <container_id> bash -c 'cd /tmp && rm -rf budget-vX.Y.Z && git clone --depth 1 --branch vX.Y.Z https://github.com/otherworld-dev/budget.git budget-vX.Y.Z'
# 2. Install production PHP dependencies IN CONTAINER
docker exec <container_id> bash -c 'cd /tmp/budget-vX.Y.Z/budget && composer install --no-dev --optimize-autoloader --no-interaction'
# 3. Build JavaScript ON HOST (npm not in container)
cd budget
git checkout vX.Y.Z # Checkout the tag on host
npm run build # Builds to js/ and css/ directories
# 4. Create clean build directory with rsync exclusions (matching Makefile)
docker exec <container_id> bash -c 'cd /tmp/budget-vX.Y.Z/budget && rm -rf /tmp/budget-build && rsync -a \
--exclude=".git" \
--exclude="build" \
--exclude="tests" \
--exclude="node_modules" \
--exclude="src" \
--exclude="webpack.config.js" \
--exclude="package.json" \
--exclude="package-lock.json" \
--exclude="composer.json" \
--exclude="composer.lock" \
--exclude="Makefile" \
--exclude=".gitignore" \
--exclude=".eslintrc.js" \
--exclude="psalm.xml" \
--exclude="*.log" \
./ /tmp/budget-build/'
# 5. Copy built JS and CSS from host to container
docker cp budget/js <container_id>:/tmp/budget-build/
docker cp budget/css <container_id>:/tmp/budget-build/
# 6. CRITICAL: Remove problematic .htaccess file
# This file causes integrity check failures (removed in v1.2.3)
docker exec <container_id> bash -c 'rm -f /tmp/budget-build/vendor/tecnickcom/tcpdf/tools/.htaccess'
# 7. Verify no development files in build
docker exec <container_id> bash -c 'find /tmp/budget-build -name "package.json" -o -name "composer.json" -o -name "Makefile" -o -name "webpack.config.js" | wc -l'
# Should output: 0
# 8. Copy signing certificates to container (if not already there)
docker cp ~/.nextcloud/certificates/budget.key <container_id>:/tmp/budget.key
docker cp ~/.nextcloud/certificates/budget.crt <container_id>:/tmp/budget.crt
# 9. Sign app to create internal signature.json
docker exec <container_id> bash -c 'chmod -R 777 /tmp/budget-build && php occ integrity:sign-app --privateKey=/tmp/budget.key --certificate=/tmp/budget.crt --path=/tmp/budget-build'
# 10. Create tarball (rename build dir to 'budget' first)
docker exec <container_id> bash -c 'cd /tmp && rm -rf budget && mv budget-build budget && tar -czf budget-vX.Y.Z.tar.gz budget'
# 11. Generate app store signature (CRITICAL - this is what the app store validates)
docker exec <container_id> bash -c 'openssl dgst -sha512 -sign /tmp/budget.key /tmp/budget-vX.Y.Z.tar.gz | openssl base64 -A'
# Save this signature output for app store submission
# 12. Copy tarball to host and rename to budget.tar.gz
docker cp <container_id>:/tmp/budget-vX.Y.Z.tar.gz ./budget.tar.gz
# 13. Create GitHub release and upload tarball
gh release create vX.Y.Z --repo otherworld-dev/budget --title "vX.Y.Z" --notes "Release notes here"
gh release upload vX.Y.Z budget.tar.gz --repo otherworld-dev/budget
# 14. Return to master branch on host
git checkout masterApp Store Submission:
- Go to https://apps.nextcloud.com/developer/apps/releases/new (or navigate via https://apps.nextcloud.com/developer/apps → Find "Budget" app → Releases → New Release)
- Download URL:
https://github.com/otherworld-dev/budget/releases/download/vX.Y.Z/budget.tar.gz - Signature: Paste the base64 string from step 11 (openssl command output)
- Submit
Critical Success Factors:
- ✅ Build from git tag, not working directory - Ensures clean, reproducible builds
- ✅ Use rsync exclusions matching Makefile - Excludes all development files
- ✅ Remove .htaccess file - Prevents integrity check failures
- ✅ Sign BEFORE creating tarball - signature.json must be inside the tarball
- ✅ Use openssl signature for app store - NOT the internal signature.json content
Common Issues:
- "Signature is invalid" error: Using wrong signature - must use openssl dgst output (step 11), not signature.json
- "Invalid signature" after reinstall: App store requires tarball named
budget.tar.gz(not versioned name) - Integrity check failures: .htaccess file still present, or dev files included (package.json, etc.)
- Development files in tarball: Built from working directory instead of clean git tag
- Certificate errors: Certificates not from Nextcloud Code Signing Intermediate Authority (serial 4835)
Verification Commands:
# Verify no development files in tarball
tar -tzf budget.tar.gz | grep -E "(package\.json|composer\.json|Makefile|webpack\.config|tests/)" | wc -l
# Should output: 0
# Verify .htaccess is NOT in tarball
tar -tzf budget.tar.gz | grep "\.htaccess" | wc -l
# Should output: 0
# Verify signature.json exists
tar -tzf budget.tar.gz budget/appinfo/signature.json
# Should find the file
# Verify no dev dependencies
tar -tzf budget.tar.gz | grep -E "vendor/(myclabs|phpunit|psalm|nextcloud/ocp)" | wc -l
# Should output: 0
# Check tarball size (should be ~16 MB for v2.0.4)
ls -lh budget.tar.gzThree-Layer Architecture:
- Controller Layer (
lib/Controller/) - HTTP request handling, API endpoints - Service Layer (
lib/Service/) - Business logic, orchestration - Data Layer (
lib/Db/) - Database access via Mappers (Repository pattern)
Key Directories:
lib/Controller/- API controllers (AccountController, TransactionController, etc.)lib/Service/- Business logic servicesService/Import/- Bank statement import subsystem (CSV, OFX, QIF parsers)Service/Forecast/- Balance forecasting and trend analysisService/Report/- Report generation and aggregationService/Bill/- Recurring bill detection and tracking
lib/Db/- Entity models and Mappers- Entities extend
Entityfrom Nextcloud - Mappers extend
QBMapperand handle all database queries
- Entities extend
lib/Enum/- Type-safe enumerationslib/Migration/- Database schema migrations (versioned)lib/AppInfo/Application.php- Dependency injection container registrationlib/BackgroundJob/- Cron jobs (cleanup, notifications)
Dependency Injection:
All services and mappers are registered in lib/AppInfo/Application.php::register(). When adding new services:
- Create the service class
- Register it in
Application.phpwith dependencies - Use
$context->registerServiceAlias()for easier DI references
Database Access Pattern:
- Each entity has a corresponding Mapper (e.g.,
Account+AccountMapper) - Mappers use Nextcloud's
QBMapperwith query builders (not raw SQL) - Use
QueryFilterBuilderfor complex transaction filtering - All queries are user-scoped (use
userIdin WHERE clauses)
Modular Architecture (Refactored in v1.2+): The frontend uses a feature-based modular architecture with ES6 imports:
Entry Point:
- main.js (~3,300 lines) - Main BudgetApp class that:
- Initializes all feature modules
- Manages global application state (accounts, categories, transactions, settings)
- Coordinates Router and module communication
- Handles session management and authentication state
Feature Modules (src/modules/):
Each module is self-contained with its own UI, logic, and API interactions:
- accounts/ - AccountsModule: Account management and reconciliation
- auth/ - AuthModule: Password protection and session management
- bills/ - BillsModule: Recurring bill detection and tracking
- categories/ - CategoriesModule: Category hierarchy and management
- dashboard/ - DashboardModule: Dashboard widgets and layout
- forecast/ - ForecastModule: Balance forecasting and trend analysis
- import/ - ImportModule: CSV/OFX/QIF import system
- income/ - IncomeModule: Recurring income tracking
- pensions/ - PensionsModule: Pension account tracking and forecasts
- reports/ - ReportsModule: Financial reports and visualizations
- rules/ - RulesModule: Auto-categorization import rules
- savings/ - SavingsModule: Savings goals management
- settings/ - SettingsModule: App settings and preferences
- shared-expenses/ - SharedExpensesModule: Shared expense tracking with contacts
- tagsets/ - TagSetsModule: Tag sets for categories
- transactions/ - TransactionsModule: Transaction CRUD and filtering (largest module ~67KB)
Core Infrastructure (src/core/):
- Router.js - Hash-based client-side routing, navigation handling
Shared Utilities (src/utils/):
- api.js - ApiClient wrapper around @nextcloud/axios
- dom.js - DOM manipulation helpers
- formatters.js - Currency, date, and number formatting
- helpers.js - General utility functions
- validators.js - Form validation utilities
Configuration (src/config/):
- dashboardWidgets.js - Dashboard widget definitions and settings (28+ widgets)
Build Output:
- Webpack bundles to
js/budget-main.jsandcss/budget-main.css - Source maps in development mode (
npm run dev) - Production builds minified and optimized
Key Architecture Patterns:
- Module Pattern: Each module is a class instantiated by BudgetApp with a reference to the parent app
- Event-Driven: Modules communicate via DOM custom events (e.g.,
transaction:created,account:updated) - Centralized State: BudgetApp maintains shared state (accounts, categories, etc.) accessible by all modules
- Router Integration: Router triggers module-specific render methods based on URL hash
- API Integration: Modules use shared ApiClient for consistent @nextcloud/axios usage
- Widget Registry: Dashboard tiles defined in DASHBOARD_WIDGETS config (hero tiles, chart widgets, summary cards)
Making Frontend Changes:
- Identify the relevant module in
src/modules/[feature]/ - Edit the module file (most are single-file modules, largest is TransactionsModule.js at ~67KB)
- For shared functionality, edit utilities in
src/utils/or add to BudgetApp insrc/main.js - Rebuild using
npm run devornpm run watchfor automatic rebuilds - Test changes by refreshing the browser after rebuild
- For cross-module communication, use custom DOM events (follow existing patterns)
Core Tables:
budget_accounts- Bank accounts, credit cards, cash, investment accounts. Includeslast_reconciledtimestamp.budget_transactions- Individual transactions with categorization, reconciliation status, and transfer linking (linked_transaction_id)budget_tx_splits- Split transactions across categoriesbudget_categories- Hierarchical category tree. Supportsexcluded_from_reportsflag to hide from budgets/reports.budget_tag_sets/budget_tags/budget_transaction_tags- Tag sets for multi-dimensional category trackingbudget_import_rules- Auto-categorization rules for importsbudget_bills- Recurring bill and transfer tracking with auto-paybudget_recurring_income- Expected income sourcesbudget_savings_goals- Savings targetsbudget_settings- Per-user key-value settingsbudget_auth- Password protection (bcrypt hashed, session tokens)budget_audit_log- Audit trail for financial actionsbudget_contacts/budget_expense_shares/budget_settlements- Shared expense tracking with multi-currency supportbudget_assets/budget_asset_snaps- Non-liquid asset tracking with value snapshotsbudget_pensions/budget_pen_snaps/budget_pen_contribs- Pension tracking and projectionsbudget_exchange_rates/budget_manual_rates- Currency exchange rates (ECB/CoinGecko + manual overrides)budget_nw_snaps- Net worth history snapshotsbudget_shares/budget_share_items- Data sharing between Nextcloud usersbudget_interest_rates- Account interest rate historybudget_bc/budget_bam- Bank sync connections and account mappingsbudget_bgt_snapshots- Per-month budget overrides
Migration System:
- Migrations in
lib/Migration/Version*.php - Versioned naming:
Version{padded_version}Date{YYYYMMDD} - Applied automatically on app enable/upgrade
- Manual execution:
php occ migrations:execute budget
CRITICAL: Boolean Column Requirements
Nextcloud's DBAL requires all boolean columns to have 'notnull' => false for compatibility across MySQL, PostgreSQL, and SQLite. This is a strict requirement that will cause installation failures if violated.
Correct:
$table->addColumn('is_active', Types::BOOLEAN, [
'notnull' => false, // REQUIRED for boolean columns
'default' => false,
]);WRONG (will cause installation errors):
$table->addColumn('is_active', Types::BOOLEAN, [
'notnull' => true, // ❌ NEVER use notnull => true on boolean columns
'default' => false,
]);Why this matters:
- PostgreSQL and some database configurations interpret boolean
NOT NULLconstraints differently - Nextcloud's DBAL abstraction layer cannot reliably handle non-nullable booleans across all databases
- Users will see errors like:
Column "table"."column" is type Bool and also NotNull, so it can not store "false" - This has caused multiple release failures (v2.1.0, v1.0.18-1.0.27)
If you create a boolean column with notnull => true:
- Fix the migration immediately by changing
'notnull' => trueto'notnull' => false - Create a cleanup migration that drops and recreates the column (see Version001000028 as example)
- Test on fresh install AND upgrade scenarios
- Check ALL recent migrations for this issue before releasing
Transfers between accounts create linked transaction pairs (linked_transaction_id). Key rules:
- Credit side gets no category — only the debit (withdrawal) carries
categoryIdfor spending aggregation. ThelinkTransactions()service method enforces this. - Dashboard transfer exclusion — transfers between non-liability accounts (e.g., checking↔savings) are excluded from income/expense totals. Transfers involving liability accounts (debt payments) count as real expenses.
- Auto-matching after import — the import flow runs
bulkFindAndMatch()after import to auto-link transfer pairs across accounts.
Categories can be flagged with excluded_from_reports = true. These are filtered out of:
- Budget analysis (
CategoryService::getBudgetAnalysis) - Budget alerts (
BudgetAlertService) - Spending reports (
ReportAggregator::getBudgetReport,generateSummary)
Useful for investment adjustments, internal bookkeeping, and reimbursement categories.
API routes defined in appinfo/routes.php:
- RESTful conventions: GET (read), POST (create), PUT (update), DELETE (destroy)
- All API routes prefixed with
/api/ - Route naming:
{controller}#{action}maps to{Controller}Controller::{action}()
Example: ['name' => 'account#show', 'url' => '/api/accounts/{id}', 'verb' => 'GET']
→ Maps to AccountController::show($id)
This app is fully translatable. All user-facing strings must be wrapped in translation functions — never use raw string literals for UI text.
- PHP:
$this->l->t('Your string')(injectIL10Nvia constructor) - JavaScript:
t('budget', 'Your string')(import from@nextcloud/l10n) - Use positioned placeholders (
%1$s,%2$s) in PHP, named placeholders ({name}) in JS - Never concatenate translated strings — use placeholders instead
- See the "Translation & i18n" section below for full details
Use Nextcloud's JSON response helpers in controllers:
use OCP\AppFramework\Http\JSONResponse;
// Success
return new JSONResponse(['data' => $result]);
// Error
return new JSONResponse(['error' => 'Message'], 400);Services should throw exceptions that controllers catch and convert to appropriate HTTP responses.
The ValidationService provides common validation utilities. For database constraints, use validation before attempting saves to provide better error messages.
All operations are user-scoped. Controllers receive userId from the framework:
public function index(): JSONResponse {
$userId = $this->userId; // Available in controller
// ...
}Services and Mappers receive userId as parameters.
Recently Added (v1.2): Categories can now have tag sets for additional classification dimensions.
TagSetentity +TagSetMapper(registered in Application.php lines 309-312)TagSetServicehandles business logic (lines 324-331)TagSetControllerprovides API endpointsTransactionTagServicefor associating tags with transactions (lines 333-341)- Routes:
/api/tag-sets,/api/categories/{id}/tag-sets - Database tables:
budget_tag_sets,budget_tags,budget_transaction_tags
Multi-stage import process:
- Upload - Store file temporarily
- Preview - Parse and show sample data
- Execute - Import with duplicate detection
- Rollback - Undo import if needed
Supports CSV (custom mapping), OFX, and QIF formats. Uses Service/Import/ subsystem.
CSV Import Enhancements (Unreleased):
- Auto-detection of delimiters (comma, semicolon, tab)
- Dual-column amount mapping for separate income/expense columns
- European number format support (1.234,56)
- Smart validation (single amount XOR dual columns)
ParserFactoryandTransactionNormalizerhandle format variations
- Unit tests in
tests/Unit/ - PHPUnit 10 configuration in
tests/phpunit.xml - Test structure mirrors lib/ directory
- Mocking with PHPUnit's
createMock() - Currently focused on recent features (tag sets have full test coverage)
- All data user-scoped (multi-tenant isolation)
- Sensitive data (account numbers) encrypted via
EncryptionService - Password Protection (v1.2.0+): Optional app-level password with session management
AuthServicehandles authentication, session tokens (64-char random)AuthMapperstores bcrypt-hashed passwords inbudget_authtable- Failed attempts tracked (5 fails = 5-minute lockout)
- Configurable session timeout (15/30/60 minutes)
- Rate limiting on auth endpoints
- Factory Reset: Complete data deletion via
FactoryResetService(preserves audit logs) - Audit logging for all financial actions (
AuditService) - CSRF protection handled by Nextcloud framework
When writing queries:
- Use
COALESCE()instead ofIFNULL()(MySQL-specific) - Avoid MySQL-specific functions
- Use parameter binding (always) for type safety
- Test on both MySQL and SQLite if modifying queries
- Main branch:
master - Feature branches:
feature/descriptionorrefactor/description - Commit style: Conventional commits (feat:, fix:, test:, refactor:, etc.)
- Current architecture: Modular frontend with 16 feature modules in
src/modules/
- Missing DI Registration: New services/mappers must be registered in
Application.phpor they'll fail at runtime - User Scoping: Always filter by
userIdin Mapper queries - Database Types: Different behavior between MySQL and SQLite (test both)
- TCPDF Autoloading: Composer autoloader loaded in
Application.phpconstructor - Tag Sets Registration: TagSetMapper and related mappers are already registered (lines 309-341 in Application.php)
- Rebuild Required: Changes to
src/files requirenpm run buildbefore testing (or usenpm run watch) - Modular Architecture: Frontend is organized by feature - each module in
src/modules/handles its own view and logic - Module Communication: Use custom DOM events for cross-module communication (see existing patterns in modules)
- Centralized State: BudgetApp in main.js maintains shared state - modules access via
this.app.accounts,this.app.categories, etc. - Chart.js Integration: Chart.js is imported in main.js - available globally in modules
- API Errors: API calls return promises - always use try/catch or .catch() for error handling
- Session Tokens: Password protection stores session tokens in localStorage - cleared on logout/timeout
The app is fully internationalized using Nextcloud's standard translation system.
Contributing a new translation:
- Download the template file from
translationfiles/templates/budget.pot(generated viamake translations) - Copy it to
translationfiles/[language_code]/budget.po(e.g.,it/budget.pofor Italian) - Translate all strings in the
.pofile using a tool like Poedit or manually - Submit a pull request with the new
.pofile - Maintainers will compile it using
php translationtool.phar convert-po-files
Updating an existing translation:
- Edit the appropriate
translationfiles/[language_code]/budget.pofile - Submit a pull request with your changes
- Maintainers will recompile the translations
Adding new translatable strings:
Backend (PHP):
// In controllers - inject IL10N via constructor
use OCP\IL10N;
private IL10N $l;
// Then use:
$this->l->t('Your string here')
$this->l->t('String with %1$s placeholder', [$variable])
$this->l->n('%n item', '%n items', $count) // pluralFrontend (JavaScript):
// Import translation functions
import { translate as t, translatePlural as n } from '@nextcloud/l10n';
// Then use:
t('budget', 'Your string here')
t('budget', 'String with {variable} placeholder', { variable: value })
n('budget', '%n item', '%n items', count) // pluralTemplates (PHP):
<?php p($l->t('Your string here')); ?>
<?php p($l->t('String with %1$s', [$variable])); ?>Extracting strings and compiling translations:
# Extract all t() and n() calls into .pot template
make translations
# OR manually:
php translationtool.phar create-pot-files
# Compile .po files to .json/.js for production
php translationtool.phar convert-po-filesImportant patterns:
- Use descriptive strings, not codes
- Keep HTML outside translation strings where possible
- Use positioned placeholders (
%1$s,%2$s) for reordering flexibility - Add
// TRANSLATORScomments for context - Never split sentences across multiple
t()calls