Skip to content
Building Your Application

Building Your Application

Your application is an ordinary Spring Boot project that happens to have the whole BigLedger platform on its classpath. This page covers how to structure it, how to add your own functionality, and what you may and may not change.

Project structure

Keep everything you write inside your own package. Nothing from BigLedger is copied into your repository.

your-api/
├── build.gradle.kts          # declares the BigLedger dependencies
└── src/main/java/com/yourcompany/
    ├── YourApplication.java  # the launcher
    ├── controller/           # your REST endpoints
    ├── service/              # your business logic
    └── config/               # your configuration

The launcher

One class starts both platforms. Registering BigLedger’s WebserviceApplication as a Spring source brings in every BigLedger controller, service and background queue; your own @SpringBootApplication class contributes everything in your package.

package com.yourcompany;

import app.api.WebserviceApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;

import java.util.Map;

@SpringBootApplication
public class YourApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder()
                .sources(WebserviceApplication.class, YourApplication.class)
                .properties(Map.of("server.port", 5000))
                .run(args);
    }
}

The application this produces serves the complete BigLedger API and your own endpoints from a single process.

Adding a REST endpoint

Write a normal Spring @RestController and wrap the body in EndpointMethod.AuthenticatedTenantEndpoint — the same handler BigLedger’s own controllers use. The wrapper checks the token and works out the tenant before your code runs. It does not decide whether this user may see this data. BigLedger’s own handlers make that decision inside the handler, with UserPermissionService.isUserPermitted, and so must yours. The example copies its permission list from the standard routes that read suppliers, so it refuses the people those routes refuse.

package com.yourcompany.controller;

import app.api.core2.common.EndpointMethod;
import com.bigledger.core2.auth.AuthTokenFields;
import com.bigledger.core2.common.api.ApiResponse;
import com.bigledger.core2.common.api.constants.Core2Config;
import com.bigledger.core2.common.api.constants.ResponseCodeConstants;
import com.bigledger.core2.common.api.constants.permissions.AkaunTenantPermissions;
import com.bigledger.core2.common.ddd.TenantInfo;
import com.bigledger.core2.dal.util.DbConnection;
import com.bigledger.core2.domain.user.UserPermissionService;
import com.google.common.collect.ImmutableList;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

import java.util.List;

@RestController
@RequestMapping(Core2Config.TENANT_DOMAIN_URL_PREFIX + "x-yourcompany/suppliers")
public class SupplierSummaryController {

    @GetMapping("/status-summary")
    public ResponseEntity<StreamingResponseBody> statusSummary(HttpServletRequest request) {

        EndpointMethod.AuthenticatedTenantEndpoint handler =
                (AuthTokenFields auth, TenantInfo tenantInfo, DbConnection masterDb) -> {

                    DbConnection tenantDb = tenantInfo.dbConn;

                    // Decide first whether this user may read suppliers in this tenant.
                    // The list is the one the standard supplier routes ask for.
                    final List<AkaunTenantPermissions> required = ImmutableList.of(
                            AkaunTenantPermissions.TNT_LOG_SUPPLIER_OWNER,
                            AkaunTenantPermissions.TNT_LOG_SUPPLIER_ADMIN,
                            AkaunTenantPermissions.TNT_API_SUPPLIER_READ);
                    if (!UserPermissionService.isUserPermitted(auth, required, false, ImmutableList.of(), tenantDb)) {
                        return EndpointMethod.notAuthorizedResponse;   // 403, CLIENT_AUTH_USER_NOT_AUTHORIZED
                    }

                    // Your logic here. You have the authenticated user, the tenant's
                    // database connection, and every BigLedger service and unit of work.

                    ApiResponse<String> response = new ApiResponse<>(
                            ResponseCodeConstants.OK_RESPONSE,
                            "Summary for tenant " + tenantDb.getTenantCode(),
                            "OK");

                    return ResponseEntity.ok()
                            .contentType(MediaType.APPLICATION_JSON)
                            .body(response.asStreamingResponse());
                };

        return handler.writeResponse(request);
    }
}

Inside the handler you receive:

ObjectWhat it gives you
AuthTokenFieldsThe authenticated user and their claims
TenantInfoThe current tenant, including tenantInfo.dbConn, its database connection
DbConnection (third parameter)The master database connection

Pass the tenant’s connection to isUserPermitted, never the master one: the roles it reads are the ones the tenant assigns in the Tenant Admin Applet. The check passes for the tenant’s owners and admins, and for platform system administrators, whatever the list says.

URL naming: use your own namespace

Your endpoints share a URL space with BigLedger’s. The example builds its path the way BigLedger’s own tenant controllers do, on the platform’s tenant prefix (Core2Config.TENANT_DOMAIN_URL_PREFIX), so it sits beside the standard tenant routes. To keep a future BigLedger release from colliding with a path you own, include a segment of the form x-<yourcompany> in every route you add.

…/x-yourcompany/suppliers/status-summary     ✅ free today
…/suppliers/status-summary                   ❌ may collide with a future release

No BigLedger route uses a path segment beginning with x- — checked across the roughly 18,700 routes resolved from the platform’s controllers — so it is free for you to claim. BigLedger has not published a commitment to keep it that way; if you need one, ask.

If you map a route that exactly matches an existing BigLedger route and HTTP method, the application fails to start with an ambiguous mapping error. This is deliberate: it prevents a customer endpoint from silently replacing a platform one.

Reusing BigLedger services

Everything the standard endpoints use is available to you. Domain services, units of work, table classes and containers can all be called directly, using the tenant DbConnection from your handler.

This is the recommended way to build features that combine BigLedger behaviour with your own: call the platform service, then apply your logic around the result, rather than reimplementing what the platform already does.

Your own data

Tables you create belong to your application. Keep them in your own namespace so they are never confused with platform tables and never collide with a future BigLedger migration — for example prefix them yc_ for “yourcompany”.

Do not add columns to BigLedger tables. Sixteen of them carry a custom_field_json column intended for exactly this purpose — including the generic-document header and line, the entity and item masters, the price book and the pricing-scheme tables. Use it for values that belong to an existing record.

Changing existing behaviour

The current release supports adding to the platform. It does not provide published extension points for changing how existing BigLedger endpoints and services behave.

If you need built-in behaviour to work differently, your options today are:

  1. Compose instead of modify. Add your own endpoint in your namespace that calls the same BigLedger services with your logic wrapped around them, and point your client at it. The built-in endpoint continues to exist and is unaffected.
  2. Talk to BigLedger. Extension points — hooks in document lifecycles, query filters, replaceable strategies — do not exist today, and no commitment to build them has been published. If you need one, say which one and why; requirements are what decides whether it gets built.
Do not attempt to work around this by patching platform classes or shadowing them on the classpath. Anything built that way is unsupported and is likely to break on your next upgrade.

One process, the whole platform

Your application is a standard Spring Boot project. It declares the BigLedger libraries as dependencies and registers BigLedger’s application class, WebserviceApplication, as a Spring source. At startup, your application serves every BigLedger endpoint, plus every endpoint you have written yourself, against the same databases, the same authentication and the same tenant model. At the backend commit these pages describe, that is about 18,700 routes from some 2,200 controllers, and none of them was written by you.

Because BigLedger arrives as a compiled dependency rather than a branch of source code, upgrading is a version change. There is no merge, and no BigLedger file in your repository that could conflict with an upgrade.

Behind the scene: one request to your endpoint

An endpoint you write the way Adding a REST endpoint shows hands its work to EndpointMethod.AuthenticatedTenantEndpoint. That is the same wrapper BigLedger’s own controllers use. Between the request arriving and your code running, the wrapper does five things:

  1. It checks the caller’s token. A token that does not verify is answered 403 with CLIENT_AUTH_INVALID_TOKEN, and your code never runs. With no token at all, it tries an access ID and key pair instead, so an integration’s key reaches your endpoint the same way a login does.
  2. It reads the tenant code. It looks in the path, then a header, then a query parameter, under tenantCode, tenant-code or tntCode. With none, the caller gets 400 and CLIENT_TENANT_CODE_IS_EMPTY.
  3. It finds that tenant’s database. It looks the code up in the tenant registry of the master database your application is connected to. An unknown code gets 404 and CLIENT_TENANT_CODE_DOES_NOT_EXIST. A registered tenant with no database row gets 404 and CLIENT_GENERAL_MISSING_DATA. A successful lookup is remembered for ten minutes, so a tenant moved to a new database server reaches your process up to ten minutes late.
  4. It calls your handler with three things: the caller’s token fields (AuthTokenFields), the tenant (TenantInfo, whose connection is tntInfo.dbConn), and the master database connection.
  5. It turns your failures into the standard envelope. A data-consistency or bad-request exception becomes a 400 with its own code. Anything else becomes a server error, so a client of your endpoint parses one shape whichever route it calls.

What the wrapper does not decide is whether this caller may see this data. BigLedger’s own handlers make that decision inside the handler. They call UserPermissionService.isUserPermitted with the tenant’s connection and answer EndpointMethod.notAuthorizedResponse (403, CLIENT_AUTH_USER_NOT_AUTHORIZED) when it returns false. Your handler needs the same call. The simplest way to choose the permission list is to copy it from the standard route that reads the same data: your endpoint then refuses exactly the people that route refuses. The check passes for the tenant’s owners and admins, and for platform system administrators, whatever the list says.

What your process takes on when it loads the platform

Loading WebserviceApplication does more than register routes. Three consequences are invisible from your own code:

  • Your process starts doing scheduled work. The moment the class loads, it starts a background thread. Every five seconds that thread claims waiting events from the master database’s app_scheduler_queue and runs them in your process. Those events are an older platform clock path. One tick fans out to every active tenant, reads that tenant’s clocks (the ones the Scheduler applet shows) and fires the due ones this path handles. Each event is claimed by exactly one process, so every application started from the library takes a share of that work for whatever database it points at. That share includes yours. The source marks this path for retirement, but at the commit these pages describe it is live.
  • Finalising from your code starts the same chain a screen does. Finalising a document queues its primary job processor. That processor fans out to secondary processors that post stock, journals, the cashbook, membership points and more, and costing is written back onto the document line afterwards. Your call returns once the work is queued, not when it is done. Automation describes the processors.
  • Starting your application does not migrate any database. Schema changes run only when a tenant database is created or a platform system administrator calls the migration route, so when you move to a newer release, the databases you run against must already have been migrated by whoever administers them (Versioning & Upgrades, step 4 has the call).

A worked scenario: GadgetSphere’s branch dashboard

GadgetSphere Sdn Bhd runs its own application on the platform library. Its IT team wants one read-only endpoint for a tablet dashboard in each of its 22 branches: today’s finalised sales for one branch. They map it under the tenant prefix /core2/tnt/, inside their own x-gadgetsphere segment, and the Klang Valley branch manager’s tablet calls it with GS-KV-01:

GET …/x-gadgetsphere/branch-dashboard/{branchCode}/today

What the endpoint depends on. akaun-api and javasdk declared at the same version, and a master database connection from BigLedger. It also needs a token from the manager’s own login, so the dashboard signs in the way any BigLedger client does. The x-gadgetsphere segment keeps the route clear of BigLedger’s: at the commit these pages describe, no BigLedger route uses an x- segment. BigLedger has not promised to keep it that way.

What the team gets for free. Token checking, tenant routing and the tenant’s database connection, all before their handler runs. They also get the response envelope every other route uses, and the entire standard API served from the same process. The dashboard’s sign-in and every other screen-like call need no code of theirs. Branch is a column on every document header and line (guid_branch), so “one branch’s sales” is a filter on data already stored, not something to invent.

What the team must not assume:

  • That the wrapper checks the manager may see sales. It does not. The team copies the permission list from the standard route that lists sales documents and calls isUserPermitted before reading anything. A login with no role in the tenant then gets 403 instead of the branch’s takings.
  • That every figure on a document is final when the document is. The sales amounts are stored on the document itself. Cost is written back onto each line by the processor chain afterwards. A margin tile that reads cost seconds after a sale can show a cost the chain has not written yet. The team shows sales on the live tile and margin on a tile that reads yesterday’s documents.
  • That a developer’s laptop is a private sandbox. Pointed at the shared test database, a developer’s copy starts running that environment’s clock ticks alongside the test servers. The team gives each developer a database of their own.

Running your application

Your application needs the same database configuration a BigLedger deployment needs. BigLedger connects to the master database during startup, so these must be present before the application starts:

VariableMeaning
DB_HOSTMaster database host
DB_PORTMaster database port
DB_USERDatabase user
DB_PASSDatabase password
DB_NAMEMaster database name

Build and run:

./gradlew bootJar
java -jar build/libs/your-api.jar

Your BigLedger contact provides the connection details for your development environment. Never point a development build at a production database — and do not share a database with anyone else’s process either: a development copy pointed at a shared test database takes a share of that environment’s clock ticks and webhook work, and the test servers silently do less of it. Give each developer a database of their own. What your process takes on explains why.

Starting your application does not migrate the database either. Versioning & Upgrades explains who runs migrations and when.

What success looks like

  • One version, three artifacts. ./gradlew dependencies --configuration runtimeClasspath | grep com.bigledger lists akaun-api, javasdk and client-sdk at the same number.
  • Your endpoint answers. Start the application and call your endpoint with the token of a user who holds one of the permissions in your list, and your tenant’s code in a tenantCode header. You get "code": "OK_RESPONSE".
  • Your endpoint refuses. Repeat the call with the token of an ordinary user who has no role in that tenant. Do not use an owner, admin or system administrator, because they pass every check. You get 403 and CLIENT_AUTH_USER_NOT_AUTHORIZED. That refusal proves your check ran against this tenant’s database. A 200 here means your handler skipped the check, and anyone with a login can read the data.
  • Routing happens before your code. Repeat the call with a tenant code that does not exist. You get CLIENT_TENANT_CODE_DOES_NOT_EXIST, and your handler’s log line never appears.
  • The platform is still there. Call a standard BigLedger endpoint you already use, with the same token, on the same port. It answers from the same process.

At that point you have a working BigLedger deployment with your own functionality inside it, scoped the way BigLedger’s own endpoints are.

Troubleshooting

When a build, a startup or a call to your endpoint fails, the symptom, cause and fix table names the fix for each message, from a 401 on a dependency to a handler that answers a user it should refuse.

Related documentation

Last updated on