AcctApp Guides

Plugins & Extending

8 topics covering plugins & extending in AcctApp.

Version 9.7.0

Plugins overview - installing & managing

Plugins extend AcctApp with custom automation, integrations, and standalone UIs - without modifying the core application.

Two plugin patterns

  1. Event-bus plugins - JavaScript that runs inside AcctApp's sandboxed Node.js vm. Subscribes to app events and calls permitted services. No file system or network access except through the service API.
  2. UI plugins - a standalone HTML/JS/CSS interface that opens in its own BrowserWindow. Gets direct file read/write access to its own data files via the window.pluginFileApi bridge. Can optionally include an event-bus entry too.

Installing a plugin

  1. Copy the plugin folder to: %APPDATA%\AcctApp\plugins\<plugin-id>\
  2. Go to Settings → System → Plugins → "Install Plugin…" → select manifest.json.
  3. The plugin appears as Inactive. Click "Permissions ▼" and tick the permissions it needs, then click "Activate".
  4. Restart AcctApp - event-bus plugins are loaded at startup.

UI plugins do not require a restart - clicking "Open UI" on the plugin card opens the BrowserWindow immediately.

Permissions

Each plugin declares which services it needs in its manifest. Only grant what it actually uses:
invoices.read / invoices.write
contacts.read / contacts.write
stock.read / stock.write
payments.read / payments.write
journals.read / journals.write
purchaseOrders.read / purchaseOrders.write
salesOrders.read / salesOrders.write

UI-only plugins (no event-bus entry) can declare an empty permissions array - they don't touch AcctApp data directly.

Development hot-reload

Set the environment variable DEV_PLUGIN_DIR to a local directory and AcctApp will load the plugin's UI from that path instead of the installed %APPDATA% copy, so edits appear on next "Open UI" without reinstalling.

Plugin manifest.json reference

Every plugin must have a manifest.json in its root folder. All fields:

{
          "id": "my-plugin",
          "name": "My Plugin",
          "version": "1.0.0",
          "author": "Your Name",
          "description": "One-line description shown in the Plugins list",
          "website": "https://example.com",
          "entry": "entry.js",
          "ui": "ui.html",
          "permissions": ["invoices.read", "contacts.read"],
          "tags": ["developer", "productivity"],
          "minAcctAppVersion": "8.0.0"
        }

Field notes

  • id - must be unique across all installed plugins; used as the folder name under %APPDATA%\AcctApp\plugins\.
    • entry - optional. Omit entirely if the plugin is UI-only.
    • ui - optional. When present, an "Open UI" button appears on the plugin card. Omit if the plugin is event-bus-only.
    • permissions - list the service permissions the event-bus entry needs. UI plugins that use only pluginFileApi can use [].
    • minAcctAppVersion - AcctApp will warn if the installed version is older than this.

A plugin can have both entry and ui - the entry runs at startup, the UI opens on demand.

Writing an event-bus plugin (entry.js)

Event-bus plugins run inside a sandboxed Node.js vm. The entry file exports an init function:

module.exports = {
          async init({ bus, services, manifest }) {
            bus.on('invoice.saved', async (data) => {
              const invoices = await services.invoices.list({ companyId: data.companyId })
              // call an external API, transform data, etc.
            })
          }
        }

Available bus events

invoice.saved · payment.recorded · contact.saved
stock.saved · purchaseOrder.saved · salesOrder.saved · app.ready

Service API - services.<name>.<verb>(...)
Methods beginning with create/update/save/delete/insert require write permission; all others require read.
Examples:
services.invoices.list({ companyId })
services.contacts.save({ companyId, data })
services.stock.list({ companyId })

Sandbox constraints

  • No require, no fs, no http - only the services API and the bus.
    • console, setTimeout, setInterval are available.
    • init() must return within 5 seconds or the plugin is disabled with an error.
    • Each event handler runs in a try/catch - unhandled rejections are logged, not fatal.

Writing a UI plugin (ui.html + pluginFileApi)

UI plugins open in a dedicated BrowserWindow with a special preload that exposes window.pluginFileApi. This gives the plugin direct access to two files in its plugin directory - data.json (the main data store) and config.json (for credentials/settings) - without any server or network round-trip.

window.pluginFileApi methods

// Read/write data.json (main persistent store)
        const json  = await window.pluginFileApi.read()          // returns string | null
        await window.pluginFileApi.write(jsonString)
        // Read/write config.json (credentials, portal URLs, etc.)
        const cfg   = await window.pluginFileApi.readConfig()     // returns string | null
        await window.pluginFileApi.writeConfig(jsonString)
        // Trigger a portal sync (see "Portal sync" below)
        const result = await window.pluginFileApi.syncPortal()    // returns { added, total }

Detecting the environment

The UI can also be opened directly in a browser (e.g. by double-clicking ui.html) for development. In that case window.pluginFileApi is undefined. Gate all IPC calls behind a check:

const useNativeApi = typeof window.pluginFileApi !== 'undefined'
        if (useNativeApi) {
          const text = await window.pluginFileApi.read()
          data = text ? JSON.parse(text) : defaultData
        } else {
          // fallback: File System Access API, localStorage, etc.
        }

File locations

Both files live alongside the plugin's other files in:
%APPDATA%\AcctApp\plugins\<plugin-id>\

The preload resolves the paths automatically - the UI never needs to know the absolute path.

Typical data.json structure

{
          "version": "1.0",
          "records": [ { "id": "CR-20260629-001", "title": "...", "status": "pending" } ],
          "metadata": {}
        }

Best practice

Always read → mutate in memory → write back. Never call write concurrently - the file I/O is synchronous on the main process side so race conditions won't corrupt the file, but logical overwrites can still lose changes if two writes race from the same UI event.

Portal sync (pluginFileApi.syncPortal)

syncPortal() is a built-in sync helper for plugins that mirror data to/from the AcctApp admin portal (admin.acctapp.com.au). It handles login, bidirectional status sync, and importing new remote records - all with a single call from the UI.

What it does (in order)

  1. Reads config.json for { url, username, password } - the portal API endpoint and credentials.
  2. Logs in to the portal (POST { action: "login" }) to get a session token.
  3. Fetches all remote records (POST { action: "cr-list" }).
  4. Pushes locally-created records that have no portal_id yet: calls cr-add for each, stores the returned portal ID in the local record as portal_id: "portal-<id>", then calls cr-update if the local status differs from the default (pending).
  5. Pushes status/notes changes for records that already have a portal_id and whose local status or claude_notes differ from the remote.
  6. Imports new remote records that don't yet exist locally (matched by portal_id), assigning a local CR-YYYYMMDD-NNN ID.
  7. Writes the updated data.json and logs out. Returns { added: N, total: M }.

config.json expected shape

{
          "url":      "https://admin.acctapp.com.au/lookup.php",
          "username": "darryn",
          "password": "••••••••"
        }

portal_id convention

Local records that originated from the portal carry "portal_id": "portal-CR-YYYYMMDD-NNN". Records created locally and later pushed to the portal also receive a portal_id after the first successful sync. Records without a portal_id are treated as "not yet synced" and will be created on the portal on the next sync.

Triggering from the UI

async function syncFromPortal() {
          if (!window.pluginFileApi) return
          const result = await window.pluginFileApi.syncPortal()
          showToast(`Sync done - ${result.added} new records imported.`)
          await loadData()  // reload UI from updated data.json
        }

API gateway (integrations)

Inbound (call AcctApp from your own code)

  1. Go to Settings → API Gateway and enable the API server.
  2. Create an API key, choosing read-only or read/write access.
  3. Call the API with that key, e.g. GET http://127.0.0.1:8910/api/v1/contacts with header Authorization: Bearer <key>. Resources: contacts, accounts, stock, invoices.

Outbound (AcctApp calls your service)

  1. Still in Settings → API Gateway, add a webhook with your URL.
  2. Choose which events trigger it, e.g. invoice.saved or payment.recorded.
  3. AcctApp POSTs a signed JSON payload to that URL whenever the event fires.

Keep the server bound to 127.0.0.1 unless you secure it behind a proxy.

SQL Query & audit

Settings → SQL Query lets a permitted (sysadmin) user run SQL against the database, useful for power users. Every statement is recorded in an append-only SQL Audit (user, time, query, outcome) that cannot be erased, even with direct database access, so there is a tamper-proof record of who changed what.

Bank feeds (CDR / Open Banking)

Automatic bank feeds via the Consumer Data Right require ACCC accreditation or a CDR-accredited aggregator (e.g. Basiq). Until then, import bank transactions via CSV (Bank Reconciliation → Import CSV).

Can't find what you need? Submit a support request and we will answer it, then add it here.