Orlando Diamond Prasetyo UNMURCE // TECHNICAL SPECS
← Portfolio Launch App β†—
SYSTEM OVERVIEW // SPECIFICATION

Unmurce Technical Documentation

Unmurce is an offline-first personal finance app. It stores transactions locally, syncs them with a cloud API, and includes Android notification parsing, AI-assisted receipt scanning, IDR/USD/EUR display, and a Neo-Brutalist interface.

⚑
Local-first transaction storage

Transactions are stored in SQLite before cloud synchronization. Recording an expense does not depend on an immediate network connection.

Live Groq routing
CONNECTING

Current model IDs from this Vercel deployment. Values refresh when this page loads.

GROQ_MODEL Loading... Default model used for receipt parsing.
GROQ_NOTIFICATION_MODEL Loading... Notification model; falls back to GROQ_MODEL when unset.

Full-Stack Technology Matrix

Technology Category Role & Architectural Responsibility
Flutter (Dart ^3.11) Mobile Client Mobile UI and local state management.
Native Kotlin Android Platform Bridge NotificationListenerService, background event queuing, and method/event channels.
SQLite (sqflite) Embedded Database Primary offline transactional ledger, setting cache, and pending sync FIFO queue.
Node.js / Express 5 Cloud Backend Stateless RESTful synchronization engine, currency gateway, and AI vision proxy.
MongoDB Atlas Cloud Database Mongoose schema-validated multi-tenant cloud storage with compound unique indices.
Firebase Authentication Identity & Auth JWT Bearer token verification middleware and multi-tenant user boundary enforcement.
Groq API AI Parsing Uses GROQ_MODEL for receipt parsing and GROQ_NOTIFICATION_MODEL for notifications; the latter defaults to GROQ_MODEL.
Azure Document Intelligence Receipt Parsing Fallback Uses the Prebuilt Receipt analyzer when a Groq failure is eligible for fallback.
Cloudflare Edge Edge DNS & Security Full SSL/TLS termination, DDoS protection, edge caching, and global routing.
Vercel Serverless Cloud Hosting Hosts the Express API as a serverless function.
AUTHOR: ORLANDO DIAMOND PRASETYO PRODUCTION STATUS: LIVE VERSION: v1.5.2
DESIGN TOKENS // UI/UX SPEC

02. Neo-Brutalism Visual Design System

The visual identity of Unmurce is anchored in modern Neo-Brutalism: unapologetic high-contrast black outlines, hard directional shadows with zero blur, deliberate asymmetry, and a signature pastel color palette.

Color Token Palette (Click any card to copy HEX)

#5DF9FF
Electric Cyan
HEX: #5DF9FF πŸ“‹
Primary accent, active status & action badges
#F9EB5D
Lemon Sunshine
HEX: #F9EB5D πŸ“‹
Hero backgrounds & food/beverage category
#FF99C8
Sakura Pink
HEX: #FF99C8 πŸ“‹
Clothing category, delete badges & alerts
#06D6A0
Mint Spring
HEX: #06D6A0 πŸ“‹
Transport category, sync confirmations & POST tags
#C77DFF
Electric Lavender
HEX: #C77DFF πŸ“‹
School supplies, books & education category
#70D6FF
Sky Blue
HEX: #70D6FF πŸ“‹
Electronics, gadgets & GET method tags

Micro-Animations: The NeoBouncy Spring Controller

Every clickable element is wrapped with the custom Flutter widget NeoBouncy. When pressed, an AnimationController with Curves.easeInOutCubic scales the widget down to 0.94 while shifting the shadow offset, providing tactile spring-loaded feedback.

Interactive Spring Demo:

Simulates Flutter NeoBouncy physics with scale compression and hard drop shadow offset absorption.

Design Token Specifications

Token CSS / Flutter Value Visual Application
Border Heavy Border.all(color: Colors.black, width: 3.5) Main cards, modals, app bars, and elevated surfaces.
Border Medium Border.all(color: Colors.black, width: 2.5) Buttons, pill chips, form inputs, and inner containers.
Hard Shadow BoxShadow(color: Colors.black, offset: Offset(4, 4), blurRadius: 0) Signature 0-blur directional shadow that elevates neo-brutalist cards.
Display Font GoogleFonts.itim() (Flutter) / Plus Jakarta Sans 800 Transaction amounts, category titles, and modal headers.
Monospace Font Fira Code / JetBrains Mono Currency rates, transaction IDs, timestamps, and JSON code blocks.

Production UI Gallery

Unmurce Main Home Screen
Home Dashboard with Expense List & Real-Time Balance
Unmurce AI Receipt Scanner Screen
Multimodal AI Receipt Scanner with Line-Item Breakdown
Unmurce Settings Screen
Settings Panel, Notification Listener & Currency Selector
SYSTEM TOPOLOGY // MULTI-TIER

03. Architecture & Data Flow

The app combines an offline-first Flutter client, an Express API hosted on Vercel, and MongoDB Atlas. Cloudflare provides edge routing for the production domain.

CLIENT TIER
Flutter Mobile Client

Handles user interactions, stores transactions in SQLite, and manages local state.

NATIVE BRIDGE
Kotlin Android Service

Background NotificationListenerService hooked into Android OS to intercept incoming transaction alerts.

SECURITY EDGE
Cloudflare + Firebase

SSL/TLS termination, DDoS mitigation, and JWT Bearer ID token cryptographic validation per request.

CLOUD CORE
Express 5 & MongoDB

Serverless Node.js backend on Vercel handling multi-tenant transactional persistence in MongoDB Atlas.

Topological Data Flow Diagram
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        ANDROID MOBILE ENVIRONMENT (DEVICE)                       β”‚
β”‚                                                                                 β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚   β”‚  Android OS Statusbar  β”‚                   β”‚   Flutter UI Layer         β”‚  β”‚
β”‚   β”‚  Banking Notifications β”‚                   β”‚   (hp2.dart, NeoBouncy)    β”‚  β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚               β”‚ Broadcast                                    β”‚                 β”‚
β”‚               β–Ό                                              β”‚ Local SQLite write β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    EventChannel / FIFO          β”‚                   β”‚
β”‚   β”‚ Kotlin NotificationSvc β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€                 β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                 β–Ό                 β”‚
β”‚                                                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚                                                β”‚ SQLite Database (sqflite)  β”‚  β”‚
β”‚                                                β”‚ my_table (synced=0/1)      β”‚  β”‚
β”‚                                                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                               β”‚ Background Sync
                                                               β”‚ (Bearer JWT)
                                                               β–Ό
                                                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                                 β”‚ Cloudflare Edge DNS / SSL  β”‚
                                                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                               β”‚
                                                               β–Ό
                                                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                                 β”‚ Vercel Serverless Express  β”‚
                                                 β”‚ requireAuth Firebase Admin β”‚
                                                 β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
                                                         β”‚             β”‚
                                        Mongoose Cluster β”‚             β”‚ AI Vision OCR
                                                         β–Ό             β–Ό
                                             β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                             β”‚ MongoDB Atlas β”‚ β”‚ Groq model             β”‚
                                             β”‚ Expense Schemaβ”‚ β”‚ (Azure receipt fallback)β”‚
                                             β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
AUTOMATION // ANDROID NOTIFICATIONS

04. Automated Notification Parser

After notification access is enabled, Unmurce processes Android payment alerts through a six-stage pipeline. Cloud parsing is attempted first, with on-device parsing available as a fallback.

The 6-Stage Hybrid Processing Pipeline

1
Native Android Gate & Deduplication (Kotlin) STAGE 1

NotificationListener.kt filters out ongoing foreground services (for example, music players and downloaders via sbn.isOngoing). It suppresses repeated notifications for 60 seconds using a content-based signature made from the app package, title, and notification text.

2
Noise & Security Filter (Local Dart) STAGE 2

Rejects sensitive authentication notifications (OTP codes, verification keys, password changes, login alerts). Employs Smart Context Filtering so promotional notifications containing purchase confirmations (e.g. "QRIS Rp 35.000 sukses, dapat diskon 10%") are accurately retained.

3
Directionality & Income Detection Filter STAGE 3

Filters out incoming transfers and top-up confirmations (e.g., "Transfer Masuk", "Top Up Berhasil", "Dana Diterima") to ensure incoming funds are not mistakenly recorded as expenditures.

4
Cloud Parser (Groq) STAGE 4

The app sends notification details to POST /api/parse-notification. The backend uses GROQ_NOTIFICATION_MODEL, falling back to GROQ_MODEL when no notification-specific model is configured.

5
On-Device Parser Fallback STAGE 5

If cloud parsing is unavailable or returns no usable result, the local Dart parser tries app-specific rules, then general financial heuristics to extract an amount and merchant.

6
Retry Queue for Unresolved Notifications STAGE 6

When cloud parsing fails or an ambiguous local result requires AI, the original notification is stored as JSON in SQLite app_settings under the pending_cloud_notifications key. The service retries queued items at startup and on its retry timer.

Apps with App-Specific Notification Parsers

BCA (myBCA / m-BCA) Mandiri Livin' BRImo (Bank BRI) BNI Bank Jago Jenius BTPN SeaBank BSI Mobile CIMB OCTO Mobile GoPay / Gojek OVO DANA Dompet Digital ShopeePay GrabPay Tokopedia

Other notification formats may be handled by the general parser when their transaction details are recognizable.

AI VISION // RECEIPT PARSING

05. Smart Receipt Scanner & Proportional Tax Allocation

When users upload or photograph a paper receipt (Indomaret, Alfamart, Starbucks, restaurants, groceries), the backend processes the image through an automated AI vision extraction pipeline.

Receipt Parser: Groq with Azure Fallback

πŸ€–
Automatic Failover Mechanism

When a Groq API key and model are configured, receipts are sent to Groq first. Eligible retryable failures fall back to the Azure Document Intelligence Prebuilt Receipt analyzer.

Proportional Tax (PPN) & Service Charge Algorithm

Receipts typically list items without tax, appending PPN (11%) and Service Charges (5-10%) at the bottom. Unmurce applies proportional allocation across all line items so every recorded item includes its exact share of tax:

Tax Distribution Mathematical Formula
LineTotal = Ξ£ (Item.Amount)
Adjustment = ReceiptTotal - LineTotal

For each Item_i (from 1 to N-1):
    ItemShare_i = Round((Adjustment * Item_i.Amount) / LineTotal)
    AllocatedTotal += ItemShare_i
    FinalAmount_i = Item_i.Amount + ItemShare_i

For the final Item_N:
    FinalAmount_N = Item_N.Amount + (Adjustment - AllocatedTotal)

Cancellable Operation & Dynamic Quantity Multiplier

On the Flutter client, requests are managed via ReceiptScanOperation wrapping http.Client. If the user taps Cancel during upload, the connection is instantly aborted (client.close()), freeing memory. Each item in the draft list maintains an interactive quantity multiplier:

Client-Side Price Calculation
int totalItemAmount = unitPrice * selectedQuantity;
EXCHANGE RATES // BASE IDR

06. Multi-Currency System & Real-Time Rates

Unmurce supports displaying amounts in three currencies: IDR, USD, and EUR. To prevent cumulative rounding errors, the system implements a strict Single Base Currency Model:

πŸ’°
Single Base Currency Rule (IDR)

Every expense is permanently stored in the database as an integer of IDR (Indonesian Rupiah). Conversions to foreign currencies are calculated dynamically on the presentation layer using reactive ValueNotifier listeners.

Supported Currencies

Currency Code Symbol Name Conversion Formula
IDR (Base) Rp Indonesian Rupiah Display = AmountInIDR Γ— 1.0
USD $ United States Dollar Display = AmountInIDR Γ— Rate_USD
EUR € Euro Display = AmountInIDR Γ— Rate_EUR

Offline Rate Caching in SQLite

Exchange rates fetched from GET /api/exchange-rates are immediately persisted to the SQLite app_settings table under the key rate_<CURRENCY>. If internet connectivity is unavailable in subsequent sessions, the app seamlessly reads from this local cache.

STATISTICS // ANALYTICS ENGINE

07. Financial Analytics & Dashboard Computations

The analytics screen (ExpenseSumarry.dart) processes transactional history into structured financial metrics with zero external analytics SDK overhead:

1. Category Spending Breakdown

Aggregates spending per category and computes proportional percentages:

Category Aggregation (Dart)
double categoryTotal = expenses
    .where((e) => e.category == targetCategory)
    .fold(0, (sum, e) => sum + e.amount);

double percentage = (categoryTotal / totalAllExpenses) * 100.0;

2. Expected vs Unexpected Expense Ratio

Categorizes spending into expected (planned essentials: meals, rent, fuel) vs unexpected (impulse purchases, emergency healthcare, spontaneous dining) to calculate financial discipline index.

3. 7-Day Spending Histogram Normalization

Groups expenses by day over the past 7 days and normalizes bar heights based on peak daily spending:

Bar Height Normalization (Dart)
final maxDailySpending = dailyTotals.reduce((a, b) => a > b ? a : b);

double computeBarHeight(double dayTotal, double maxContainerHeight) {
  if (maxDailySpending == 0) return 0.0;
  return (dayTotal / maxDailySpending) * maxContainerHeight;
}
REPORTING // VECTOR PDF

08. PDF Report Engine & Android Scoped Storage Compliance

Users can export their complete transactional ledger into formatted, formal PDF reports. The generator is built entirely with Flutter vector drawing libraries (pdf and printing).

Scoped Storage Compliance (Android 10 to 14+)

Traditional Android apps trigger dangerous storage permission prompts (WRITE_EXTERNAL_STORAGE) which are rejected on modern Android versions. Unmurce implements a modern storage pipeline:

  • MediaStore API Integration: Utilizes media_store_plus to save generated PDFs directly into the public Download/ExpenseApp directory without requiring runtime storage permissions.
  • FileSaver Fallback: Automatically invokes file_saver on devices where MediaStore direct access is restricted.
  • Instant Document Launch: Invokes open_filex to trigger the user's native PDF reader immediately upon file save.
SECURITY // MULTI-TENANCY

09. Authentication & Multi-Tenant Data Isolation

User identification is secured end-to-end via Firebase Authentication and enforced through stateless cryptographic JWT Bearer verification:

Express requireAuth Middleware (api/index.js)
async function requireAuth(req, res, next) {
  try {
    const header = req.headers.authorization || "";
    if (!header.startsWith("Bearer ")) {
      return res.status(401).json({ error: "Authentication required" });
    }
    const idToken = header.substring(7);
    req.user = await admin.auth().verifyIdToken(idToken);
    next();
  } catch (err) {
    res.status(401).json({ error: "Invalid or expired token" });
  }
}

Local SQLite Data Isolation (Multi-User Device Support)

If multiple users share the same Android device, records remain isolated. Every row in my_table stores an ownerId column corresponding to AuthService.currentUser?.uid. All queries strictly include WHERE ownerId = ? to prevent data bleed.

Single-Run Notification Onboarding Gate

To prevent user fatigue while complying with Android notification listener requirements, the permission screen is governed by a persistent SQLite flag:

One-Time Onboarding Flag (hp2.dart)
final shown = await DatabaseHelp.getSetting('notification_permission_onboarding_shown');
if (shown == null && !isPermissionGranted) {
  await DatabaseHelp.saveSetting('notification_permission_onboarding_shown', 'true');
  Navigator.push(context, SmoothPageRoute(page: const NotificationPermissionPage()));
}
OFFLINE-FIRST // RECONCILIATION

10. Offline-First Synchronization & Cloud Reconciliation

Unmurce adheres strictly to the Offline-First Paradigm. The mobile app never awaits network responses before confirming user actions.

The Synced State Protocol

Flag Value State Definition Lifecycle & Cloud Action
synced = 0 Local Mutation Pending Record exists only on device. Picked up by background worker syncPendingExpenses() to push to MongoDB.
synced = 1 Reconciled & In-Sync Record has been accepted by MongoDB Atlas and holds a valid remote mongoId.

Cloud Pull Deduplication Algorithm

When a user reinstalls the app or links a second device, the "Load Online Expenses" feature pulls all cloud records and merges them into SQLite without duplicating transactions:

Cloud Pull Deduplication (hp2.dart)
final localRows = await db.query('my_table', where: 'ownerId = ?', whereArgs: [uid]);
final localMongoIds = localRows.map((r) => r['mongoId']).whereType<String>().toSet();

for (final online in onlineExpenses) {
  final remoteId = online['_id'] as String;
  if (!localMongoIds.contains(remoteId)) {
    await db.insert('my_table', {
      'mongoId': remoteId,
      'ownerId': uid,
      'name': online['name'],
      'amount': online['amount'],
      'category': online['category'],
      'type': online['type'],
      'date': online['date'],
      'synced': 1,
    });
  }
}
DATA MODELS // SCHEMAS

11. Primary Data Models (SQLite & MongoDB)

1. Local SQLite Schema (my_db.db β€” Version 6)

Table Column Type Constraints Description
my_table id INTEGER PRIMARY KEY AUTOINCREMENT Unique local transaction identifier.
my_table mongoId TEXT NULLABLE Remote MongoDB ObjectId reference.
my_table ownerId TEXT NOT NULL Firebase UID of the account owner.
my_table name TEXT NOT NULL Title / merchant of expenditure.
my_table amount INTEGER NOT NULL Monetary amount in base IDR currency.
my_table date TEXT NOT NULL ISO date string (YYYY-MM-DD).
my_table category TEXT NOT NULL makanan, transportasi, hiburan, etc.
my_table type TEXT NOT NULL expected, unexpected, others.
my_table synced INTEGER DEFAULT 0 0 = pending push, 1 = synced to cloud.
pending_notification_expenses id INTEGER PRIMARY KEY AUTOINCREMENT Unique local notification expense identifier.
pending_notification_expenses ownerId TEXT NOT NULL Firebase UID of the account owner.
pending_notification_expenses name TEXT NOT NULL Parsed merchant or transaction name.
pending_notification_expenses amount INTEGER NOT NULL Transaction amount in IDR.
pending_notification_expenses date TEXT NOT NULL ISO date string (YYYY-MM-DD).
pending_notification_expenses category TEXT NOT NULL Expense category.
pending_notification_expenses type TEXT NOT NULL Expense classification, such as expected or unexpected.
pending_notification_expenses sourceApp TEXT NOT NULL Package name of the notification source app.
pending_notification_expenses receivedAt TEXT NOT NULL Timestamp when the notification was received.
app_settings key TEXT PRIMARY KEY Unique configuration identifier.
app_settings value TEXT NOT NULL Text or JSON-encoded setting value; the notification retry queue is stored under pending_cloud_notifications.

2. Cloud MongoDB Atlas Mongoose Schema

Mongoose Expense Schema & Compound Index
const ExpenseSchema = new mongoose.Schema(
  {
    ownerId: { type: String, required: true, index: true },
    localId: { type: String, required: true },
    name: { type: String, required: true },
    amount: { type: Number, required: true },
    category: { type: String, required: true },
    type: { type: String, required: true }, // 'expected' | 'unexpected' | 'others'
    date: { type: String, required: true }, // 'YYYY-MM-DD'
  },
  { timestamps: true }
);

// Compound unique index guarantees upsert idempotency per local ID
ExpenseSchema.index({ ownerId: 1, localId: 1 }, { unique: true });
API SPEC // REST ENDPOINTS

12. Core REST API Reference (10 Endpoints)

This section covers ten core transaction, parsing, and exchange-rate endpoints. The Flutter client defaults to https://expense-app-personal.vercel.app/api; override it with EXPENSE_API_BASE_URL when needed. Private endpoints require a Firebase ID token in the Authorization header.

GET /api/users
Auth: Bearer JWT

Fetch all expense documents belonging to the authenticated user.

Response 200 OK
[
  {
    "_id": "6643a7b9f1d2c3e4a5b6c7d8",
    "ownerId": "firebase_uid_12345",
    "localId": "42",
    "name": "Kopi Kenangan QRIS",
    "amount": 24000,
    "category": "makanan",
    "type": "expected",
    "date": "2026-09-21",
    "createdAt": "2026-09-21T08:30:00.000Z"
  }
]
POST /api/users
Auth: Bearer JWT (Upsert)

Create or upsert a new transaction using the client's localId.

Request Body
{
  "localId": "42",
  "name": "Bensin Pertamax",
  "amount": 50000,
  "category": "transportasi",
  "type": "expected",
  "date": "2026-09-21"
}
GET /api/users/:id
Auth: Bearer JWT

Retrieve a single transaction document by its MongoDB ObjectId.

PUT /api/users/:id
Auth: Bearer JWT

Update fields of an existing transaction by its MongoDB ObjectId.

DELETE /api/users/:id
Auth: Bearer JWT

Permanently remove a transaction by its MongoDB ObjectId.

POST /api/users/delete-all
Auth: Bearer JWT

Bulk delete every transaction owned by the authenticated user in a single query.

Response 200 OK
{
  "message": "Deleted",
  "deletedCount": 184
}
GET /api/users/local/:localId
Auth: Bearer JWT

Retrieve a transaction by the SQLite device localId string.

POST /api/parse-receipt
Multimodal AI OCR

Upload a base64-encoded receipt photo to extract line items with tax distribution.

Response 200 OK (NDJSON stream)
{"type":"progress","stage":"groq","status":"processing","progress":0.4,"message":"Processing with Groq"}
{"type":"result","date":"2026-09-21","provider":"groq","items":[{"name":"Iced Caffe Latte","quantity":2,"amount":62000,"category":"makanan","type":"expected"},{"name":"Croissant Butter","quantity":1,"amount":31000,"category":"makanan","type":"expected"}]}
POST /api/parse-notification
AI Cloud Semantic Fallback

Parse notification text into structured expense fields using the configured Groq model.

Request Body
{
  "title": "Transaksi Sukses",
  "packageName": "com.bca",
  "message": "Pembayaran QRIS Rp 45.000 ke RM Padang Sederhana berhasil pada 21/09"
}
Response 200 OK
{
  "name": "RM Padang Sederhana",
  "amount": 45000,
  "category": "makanan",
  "type": "expected"
}
GET /api/exchange-rates
Auth: Bearer JWT

Fetch live global currency exchange rates calculated with base currency IDR = 1.0.

Response 200 OK
{
  "base": "IDR",
  "rates": {
    "IDR": 1,
    "USD": 0.000064,
    "EUR": 0.000059
  }
}
DEV & OPS // BUILD INSTRUCTIONS

13. Setup & Build Guide

1. Backend Setup (Vercel & Express)

Use these names when configuring Vercel environment variables or the local environment used by Vercel CLI. Replace the placeholders and keep real credentials out of source control.

Backend Environment Variables
# Required; replace the placeholder values
MONGODB_URI=YOUR_MONGODB_URI
FIREBASE_SERVICE_ACCOUNT_JSON=YOUR_SERVICE_ACCOUNT_JSON
GROQ_API_KEY=YOUR_GROQ_API_KEY
GROQ_MODEL=YOUR_GROQ_MODEL_ID
# Optional: defaults to GROQ_MODEL
GROQ_NOTIFICATION_MODEL=YOUR_NOTIFICATION_MODEL_ID
AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT="https://your-resource.cognitiveservices.azure.com/"
AZURE_DOCUMENT_INTELLIGENCE_KEY="YOUR_AZURE_KEY"
Backend Terminal
# From repository root
cd backend

npm install

# Run the API locally with Vercel CLI
npx vercel dev

2. Mobile Application Build (Flutter & Android Gradle)

Bash Terminal
# Navigate to mobile Flutter project
cd frontend/expenseapp

# Pull pub packages
flutter pub get

# Run on connected Android device / emulator
flutter run

# Compile production split-per-abi release APKs
flutter build apk --split-per-abi --release
Copied to clipboard! βœ“