This application is a middleware/integration layer that synchronizes bibliographic items, holdings, and patron requests between multiple network institutions (libraries using Ex Libris Alma) and a single Shared Collection Facility (SCF) — also called the "Remote Storage Institution."
The app acts as an automated bridge: it listens for events (webhooks) from Alma, processes files exchanged via FTP, and calls Alma REST APIs to keep item records and requests in sync across institutions.
Deployment model: A Java web application (WAR) deployed on a servlet container (originally Heroku with webapp-runner). It also has a scheduled background process for log backup.
┌─────────────────────┐ ┌───────────────────────┐
│ Member Institution │ │ Remote Storage (SCF) │
│ (Alma instance) │ │ (Alma instance) │
└────────┬────────────┘ └────────┬──────────────┘
│ │
│ Webhooks (HTTP POST) │ Webhooks (HTTP POST)
│ FTP files (items/requests) │
▼ ▼
┌─────────────────────────────────────────────────────────┐
│ Alma Remote Storage App │
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ │
│ │ Webhook │ │ Items │ │ Requests │ │
│ │ Servlet │ │ Handler │ │ Handler │ │
│ └──────────┘ └──────────┘ └───────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ Alma REST API Client │ │
│ │ (Bibs, Items, Holdings, Loans, │ │
│ │ Requests, Users, Configuration) │ │
│ └────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │FTP/SFTP │ │
│ │ Client │ │
│ └──────────┘ │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────┐
│ FTP/SFTP Server │
│ (shared file area) │
└──────────────────────┘
The app loads its configuration from a JSON file (conf.json). The loading priority is:
- Environment variable
CONFIG_FILE— If set, the app fetches the config from this URL (supportsftp://URLs or any URL). This is the production approach. - Classpath resource — If the env var is not set, it looks for conf.json in the classpath (e.g., resources).
Configuration is a singleton that can be reloaded at runtime by calling GET /configuration.
{
"gateway": "<Alma API base URL, e.g. https://api-na.hosted.exlibrisgroup.com>",
"main_local_folder": "<local temp directory for file processing, e.g. /tmp>",
"report_file_folder": "<path where CSV error reports are written>",
"max_number_of_threads": "<number of parallel threads for item processing, default 4>",
"ftp_server": {
"host": "<FTP/SFTP hostname>",
"user": "<FTP username>",
"password": "<FTP password>",
"main_folder": "<root directory on FTP>",
"ftp_port": "<port: 21 for FTP, 22 for SFTP>"
},
"remote_storage_inst": "<institution code of the SCF, e.g. 01AAA_RS>",
"remote_storage_apikey": "<API key for the SCF institution>",
"remote_storage_holding_library": "<library code in SCF for holdings, e.g. RS>",
"remote_storage_holding_location": "<default location code in SCF, e.g. rs_shared>",
"remote_storage_circ_desc": "<circulation desk code for SCF loans>",
"remote_storage_digitization_department": "<digitization department code in SCF>",
"webhook_secret": "<HMAC secret for validating webhook signatures>",
"ignore_delete_files": "<true|false - whether to skip processing delete files>",
"institutions": [
{
"code": "<institution code, e.g. 01AAA_AB>",
"apikey": "<API key for this institution>",
"requests_job_id": "<Alma job ID for remote storage request export>",
"publishing_job_id": "<Alma job ID for item publishing>",
"default_library": "<fallback library code for this institution>",
"libraries": [
{
"code": "<library code, e.g. ABRS>",
"circ_desc": "<circulation desk for scan-in operations>",
"remote_storage_location": ["<location1>", "<location2>"]
}
]
}
]
}Key concepts:
remote_storage_locationarrays define which locations in a member institution map to items stored in remote storage.- The
ftp_portdetermines protocol:"22"→ SFTP, anything else → FTP. - Each institution has its own API key, job IDs, and library configuration.
Purpose: Alma sends a challenge when activating a webhook profile. The app must echo it back.
Request: GET /webhook?challenge=<value>
Response: JSON {"challenge": "<value>"}
Purpose: Receives all webhook notifications from Alma.
Flow:
- Read the full request body (JSON string).
- If
webhook_secretis configured, validate theX-Exl-Signatureheader:- Compute HMAC-SHA256 of the body using the secret as key.
- Base64-encode the result.
- Compare with the signature header. If mismatch, reject.
- Immediately respond
"Got message"to the caller. - Spawn a new thread to process the message asynchronously.
Signature validation algorithm:
signature = Base64( HMAC-SHA256( request_body_bytes, secret_bytes ) )
Compare computed value with X-Exl-Signature header.
Purpose: Triggers a fresh reload of conf.json from its source.
Response: "End Update Configuration"
Purpose: Dynamically change the application log level at runtime.
Parameters: level — one of DEBUG, INFO, WARN, ERROR, etc.
All webhook messages from Alma are JSON with these key fields:
action— one of:JOB_END,LOAN,REQUESTinstitution.value— the institution code that triggered the eventevent.value— specific event type (for LOAN and REQUEST actions)
When an Alma job finishes, the app checks which institution and job type it belongs to:
- Extract
job_instance.job_info.idfrom the message. - Search all configured institutions for a matching
publishing_job_id:- If found → call Items Synchronization (
mergeItemsWithSCF) for that institution.
- If found → call Items Synchronization (
- If not found as publishing job, search for matching
requests_job_id:- If found → call Request Processing (
sendRequestsToSCF) for that institution.
- If found → call Request Processing (
- If no match → ignore (log debug message).
Only processed when the webhook originates from the Remote Storage institution.
Purpose: When an item is returned at the SCF, notify the originating member institution so they can "scan in" the item on their end (triggering fulfillment of the original request).
Flow:
- Extract
user_idanditem_barcodefromitem_loan. - Check if barcode ends with
"X"(SCF items have barcodes = original barcode + "X"). If not, ignore. - Strip the trailing "X" to get the original barcode.
- Look up the SCF item by barcode + "X" to get its
provenance(the originating institution code). - Look up the same item in the originating institution (by original barcode).
- Scan in the item at the originating institution (triggers fulfillment workflow).
Scan-in details:
- Determine the correct library from the item's current location (or temp location).
- Look up the circulation desk from configuration.
- Call
POST /almaws/v1/bibs/{mmsId}/holdings/{holdingId}/items/{itemPid}?op=scan&library={lib}&circ_desk={desk}&done=true
Only processed when the webhook originates from the Remote Storage institution.
Purpose: When a request is canceled in the SCF, propagate the cancellation back to the originating institution.
Two sub-scenarios:
- The webhook has
user_request.barcodeending in "X". - Strip "X", look up SCF item to find provenance (originating institution).
- Look up the item in the originating institution.
- Get all requests on that item in the originating institution.
- Cancel each request with reason "Remote storage cannot fulfill the request".
- The webhook has
user_request.commentcontaining a pattern:{Source Request INSTITUTION-requestId-userPrimaryId} - Parse out the institution code, request ID, and user ID.
- Cancel the request in the originating institution using either title-level or user-level request cancellation API.
Trigger: Webhook JOB_END with a matching publishing_job_id.
Purpose: Synchronize physical item records from a member institution to the SCF. Items published from the institution via Alma's publishing profile are compared against the SCF and created/updated/deleted as needed.
- Determine local working directory:
{main_local_folder}/files/items/ - Clean the local
targz/subdirectory. - Connect to FTP/SFTP and download all files from
{ftp_main_folder}/{institution_code}/items/into localtargz/. - Extract all
.tar.gzfiles into localxml/folder. - Process each XML file using a thread pool (configurable size, default 4).
- If
ignore_delete_filesistrue, skip files containing_deletein the name.
Each XML file contains MARC21 records. For each record:
- Parse as MARC4J
Recordobjects. - Extract the Network Zone MMS ID from field
035$a(value containing "EXLNZ"). - Extract item data from repeatable field
ITM:$b= barcode$c= library code$l= location code$n= internal note 2
- If the filename contains
_delete, callitemDeleted(). Otherwise callitemUpdated().
IF item exists in SCF (lookup by barcode + "X"):
IF item is NOT in a remote-storage location (in the institution):
IF ignore_delete_files is true:
Do nothing (log: ignoring delete)
ELSE:
Delete the item from SCF
ELSE:
Update the SCF item with fresh data from the institution item
ELSE (item does NOT exist in SCF):
IF item is NOT in a remote-storage location (in the institution):
Do nothing (not relevant for remote storage)
ELSE:
Need to create the item in SCF:
1. Find or create a BIB record in SCF
2. Find or create a HOLDING record in SCF
3. Create the ITEM in SCF
4. Create a LOAN on the new item (to the institution's system user)
If Network Zone MMS ID exists:
- Search SCF bibs by NZ MMS ID:
GET /almaws/v1/bibs?nz_mms_id={id} - If not found → create bib from NZ:
POST /almaws/v1/bibs?from_nz_mms_id={id}
If NO Network Zone MMS ID:
- Search SCF bibs by "other system ID" using pattern
(INSTITUTION_CODE)LOCAL_MMS_ID:GET /almaws/v1/bibs?other_system_id={value} - If not found → create bib with the MARC record XML (adds 035 field with institution prefix, removes AVA fields):
POST /almaws/v1/bibs
- Get all holdings for the BIB:
GET /almaws/v1/bibs/{mmsId}/holdings - Look for a holding matching the SCF library and location.
- If not found → create holding with template:
<holding> <record> <datafield tag="852" ind1="0" ind2=" "> <subfield code="b">{library_code}</subfield> <subfield code="c">{location_code}</subfield> </datafield> </record> <suppress_from_publishing>false</suppress_from_publishing> </holding>
- The location is determined by checking if the item's location exists in the SCF library's locations (via API call to
/almaws/v1/conf/libraries/{lib}/locations). If not, use the default location from config.
- Retrieve the full item record from the institution:
GET /almaws/v1/items?item_barcode={barcode} - Modify the item data:
- Set barcode to
{original_barcode}X - Set
provenanceto the institution code - Remove:
po_line,library,location,policy,internal_note_1,internal_note_3,call_number,temp_library,in_temp_location,temp_location,temp_policy
- Set barcode to
- Create item:
POST /almaws/v1/bibs/{mmsId}/holdings/{holdingId}/items
After creating the item, wait 3 seconds, then create a loan:
- User ID =
{institution_code}-{library_code}(the system patron) - Loan body includes
circ_deskandlibraryfrom config POST /almaws/v1/users/{userId}/loans?item_pid={pid}
When an item already exists in both places:
- Get the institution's current item data.
- Merge: keep the SCF's
pid,barcode(with X),provenance,library,location,alternative_call_number,alternative_call_number_type,storage_location_id,internal_note_1,internal_note_3,statistics_note_2,holding_data,bib_data. - Take all other metadata from the institution item.
PUT /almaws/v1/bibs/{mmsId}/holdings/{holdingId}/items/{pid}
- Look up item in SCF by barcode + "X".
- If found → delete it:
DELETE /almaws/v1/bibs/{mmsId}/holdings/{holdingId}/items/{pid}
Check if the item's library + location matches any entry in the configured institutions[].libraries[].remote_storage_location array for the item's institution.
Trigger: Webhook JOB_END with a matching requests_job_id.
Purpose: Process remote storage request files exported by Alma and create corresponding requests in the SCF.
- Clean local working directory:
{main_local_folder}/files/requests/xml/ - Download XML files from FTP:
{ftp_main_folder}/{institution_code}/requests/ - Parse each XML file into a list of
ItemDatarequest objects. - Route each request to the appropriate handler based on type and barcode format.
- If any handler returns
false(failure), cancel the original request in the source institution.
Each request XML contains <xb:...> elements:
xb:barcode— item barcode (may be null for bib-level requests)xb:mmsId— bibliographic MMS IDxb:description— item descriptionxb:requestType— one of:HOLD,PHYSICAL_TO_DIGITIZATION,STAFF_PHYSICAL_DIGITIZATION,RESOURCE_SHARING_P2D_SHIPMENTxb:requestId— the original request IDxb:institutionCode— pick-up institution (may differ from source)xb:libraryCodeorxb:pickup > xb:library— pick-up libraryxb:patronInfo— containsxb:patronName,xb:patronIdentifier,xb:patronEmail,xb:patronAddressxb:requestNote— free-text note
Library resolution: If a library code is not directly available, the app resolves library name → code via the Alma Configuration API (GET /almaws/v1/conf/libraries). Falls back to the institution's default_library from config.
IF barcode contains space or colon (= title/bib-level):
IF type == PHYSICAL_TO_DIGITIZATION AND userId exists:
→ createDigitizationUserRequest (patron digitization at bib level)
ELSE IF type == STAFF_PHYSICAL_DIGITIZATION or RESOURCE_SHARING_P2D_SHIPMENT:
→ createSystemUserDigitizationTitleRequest
ELSE:
→ createBibRequest
ELSE IF barcode is valid (non-empty, no spaces/colons):
IF type == PHYSICAL_TO_DIGITIZATION:
→ createDigitizationItemRequest (patron digitization at item level)
ELSE IF type == STAFF_PHYSICAL_DIGITIZATION or RESOURCE_SHARING_P2D_SHIPMENT:
→ createSystemUserDigitizationItemRequest
ELSE:
→ createItemRequest
ELSE (no barcode):
→ createBibRequest
- Look up SCF item by barcode + "X".
- If not found → fail (cancel source request).
- Check process_type. If
"LOAN":- Item is currently on loan → cancel the source institution's request with note "Item is on loan".
- Return success (don't create SCF request).
- Otherwise, create request in SCF:
- Build request object: type=
HOLD, sub_type=PATRON_PHYSICAL, pickup=USER_HOME_ADDRESS, task=Pickup From Shelf - User ID =
{institution_code}-{library_code} - Comment includes: request note, patron info, request type, internal identifier
POST /almaws/v1/bibs/{mmsId}/holdings/{holdingId}/items/{pid}/requests?user_id={userId}&allow_same_request=true
- Build request object: type=
- Get the original request details from the source institution.
- Get the source institution's BIB record.
- Extract Network Zone number from
network_numberarray. - Find corresponding BIB in SCF (by NZ number or by other_system_id).
- Create bib-level request in SCF:
- User ID =
{institution_code}-{library_code} - Comment includes source institution, patron info, source request type, internal identifier, and
{Source Request INSTITUTION-requestId-userPrimaryId}tag. - Copies over:
description,manual_description,volume,issue,part,date_of_publication. - If
manual_descriptionexists, try to attach to specific holding in SCF. POST /almaws/v1/bibs/{mmsId}/requests?user_id={userId}&allow_same_request=true
- User ID =
- Get the original request from source institution.
- Get the patron info from the source institution (
GET /almaws/v1/users/{userId}?user_id_type=all_unique). - Find or create the patron as a linked user in the SCF:
- Check
source_link_id/linking_idon the institution user. - Try to find existing linked user in SCF:
GET /almaws/v1/users/{linkId}?source_institution_code={inst} - If not found, create:
POST /almaws/v1/users?source_institution_code={inst}&source_user_id={id}
- Check
- Refresh the linked user:
POST /almaws/v1/users/{userId}?op=refresh - Get SCF item by barcode + "X".
- Create digitization request in SCF:
- Type =
DIGITIZATION, sub_type =PHYSICAL_TO_DIGITIZATION - Target destination = configured
remote_storage_digitization_department - Copies:
copyrights_declaration_signed_by_patron,description,partial_digitization,required_pages_range,full_chapter,chapter_or_article_title,chapter_or_article_author,manual_description,last_interest_date,volume,issue,part,date_of_publication POST /almaws/v1/bibs/{mmsId}/holdings/{holdingId}/items/{pid}/requests?user_id={userId}
- Type =
- If successful, cancel the title request in the source institution (with reason "RequestSwitched").
Same as item-level digitization, but:
- Works with a BIB in SCF (found by NZ or other_system_id).
- Creates a bib-level digitization request.
POST /almaws/v1/bibs/{mmsId}/requests?user_id={userId}
For STAFF_PHYSICAL_DIGITIZATION or RESOURCE_SHARING_P2D_SHIPMENT:
- No real patron — uses system user ID =
{institution_code}-{library_code}. - Gets the original request, finds the SCF item.
- Creates digitization request using the system user.
- Uses system user ID.
- Gets original request, finds SCF bib.
- Creates bib-level digitization request.
If any request handler returns false, the app cancels the original request in the source institution:
- If
mmsIdis available:DELETE /almaws/v1/bibs/{mmsId}/requests/{requestId}?reason=CannotBeFulfilled¬ify_user=true¬e=Request could not be fulfilled by the SCF. - If only
userIdis available:DELETE /almaws/v1/users/{userId}/requests/{requestId}?reason=CannotBeFulfilled¬ify_user=true
- Port
22→ SFTP (using JSch library) - Any other port → FTP (using Apache Commons Net)
- Connect to server.
- List files in the specified remote directory.
- Create an
OLDsubdirectory (if files exist). - For each file:
- Rename the file to
OLD/{filename}(atomic move to prevent concurrent processing). - Download from
OLD/{filename}to local directory. - If download fails, move file back to original location.
- Rename the file to
- Disconnect.
- Upload a single local file to a specified remote path.
- Used for log backup only.
All API calls go through a central HTTP client (AlmaRestUtil) with retry logic.
- If response code is 500–505 (server error), wait 5 minutes and retry.
- Maximum retries: 36 (approximately 3 hours total).
- If response code is 429 (rate limited), do NOT retry.
- API key is appended as query parameter
apikey=. - Content-Type is auto-detected: if body is valid JSON →
application/json, otherwise →application/xml. - Accept header is always
application/json. - Connection timeout: 20 seconds. Read timeout: 500 seconds.
| API | Method | Endpoint | Purpose |
|---|---|---|---|
| Bibs | GET | /almaws/v1/bibs?nz_mms_id={id}&view=full&expand=p_avail |
Find bib by Network Zone ID |
| Bibs | GET | /almaws/v1/bibs?other_system_id={id}&view=full&expand=p_avail |
Find bib by institution system ID |
| Bibs | GET | /almaws/v1/bibs/{mmsId}?view=full&expand=None |
Get single bib |
| Bibs | POST | /almaws/v1/bibs?from_nz_mms_id={id}&validate=false&override_warning=true |
Create bib from NZ |
| Bibs | POST | /almaws/v1/bibs?validate=false&override_warning=true |
Create local bib |
| Holdings | GET | /almaws/v1/bibs/{mmsId}/holdings |
List holdings |
| Holdings | POST | /almaws/v1/bibs/{mmsId}/holdings |
Create holding |
| Items | GET | /almaws/v1/items?item_barcode={barcode} |
Find item by barcode |
| Items | POST | /almaws/v1/bibs/{mmsId}/holdings/{holdingId}/items |
Create item |
| Items | PUT | /almaws/v1/bibs/{mmsId}/holdings/{holdingId}/items/{pid} |
Update item |
| Items | DELETE | /almaws/v1/bibs/{mmsId}/holdings/{holdingId}/items/{pid} |
Delete item |
| Items | POST | /almaws/v1/bibs/{mmsId}/holdings/{holdingId}/items/{pid}?op=scan&library={lib}&circ_desk={desk}&done=true |
Scan-in item |
| Requests | POST | /almaws/v1/bibs/{mmsId}/holdings/{holdingId}/items/{pid}/requests?user_id={id}&allow_same_request=true |
Create item request |
| Requests | POST | /almaws/v1/bibs/{mmsId}/requests?user_id={id}&allow_same_request=true |
Create bib request |
| Requests | GET | /almaws/v1/bibs/{mmsId}/holdings/{holdingId}/items/{pid}/requests |
List item requests |
| Requests | GET | /almaws/v1/bibs/{mmsId}/requests/{requestId} |
Get bib request |
| Requests | DELETE | /almaws/v1/bibs/{mmsId}/holdings/{holdingId}/items/{pid}/requests/{id}?reason={r}¬e={n} |
Cancel item request |
| Requests | DELETE | /almaws/v1/bibs/{mmsId}/requests/{requestId}?reason={r}¬ify_user={bool}¬e={n} |
Cancel title request |
| Requests | DELETE | /almaws/v1/users/{userId}/requests/{requestId}?reason={r}¬ify_user={bool}¬e={n} |
Cancel user request |
| Loans | POST | /almaws/v1/users/{userId}/loans?item_pid={pid} |
Create loan |
| Users | GET | /almaws/v1/users/{userId}?user_id_type={type} |
Get user |
| Users | GET | /almaws/v1/users/{userId}?source_institution_code={inst} |
Get linked user |
| Users | GET | /almaws/v1/users/{userId}/requests/{requestId} |
Get user request |
| Users | POST | /almaws/v1/users?source_institution_code={inst}&source_user_id={id} |
Create linked user |
| Users | POST | /almaws/v1/users/{userId}?op=refresh |
Refresh linked user |
| Config | GET | /almaws/v1/conf/libraries |
List libraries |
| Config | GET | /almaws/v1/conf/libraries/{code}/locations |
List library locations |
Central data transfer object carrying information for both items and requests:
| Field | Type | Description |
|---|---|---|
barcode |
String | Item barcode (original, without "X") |
institution |
String | Institution code (pick-up institution for requests) |
library |
String | Library code |
location |
String | Location code |
networkNumber |
String | Network Zone MMS ID |
mmsId |
String | Local MMS ID in the institution |
description |
String | Item description (for requests) |
note |
String | Internal note 2 from MARC ITM field |
record |
MARC Record | Full MARC record (for bib creation when no NZ number) |
requestType |
String | Request type code |
requestId |
String | Original request ID |
userId |
String | Patron identifier |
sourceInstitution |
String | The institution that originated the request |
patron |
PatronInfo | Patron details (name, id, email, address) |
requestNote |
String | Free-text note from the request |
Critical rule: Items in the SCF always have barcode = {original_barcode}X
- When looking up an item in the SCF, append "X" to the barcode.
- When a webhook returns an SCF barcode ending in "X", strip it to get the original.
- If a barcode does NOT end in "X" in the SCF context, it's not part of this system — ignore it.
- System users (for physical requests): ID format is
{institution_code}-{library_code}(e.g.,01AAA_AB-ABRS). These must be pre-created in the SCF with a home address. - Linked patron users (for digitization): Created dynamically in the SCF by linking to the source institution user via
source_institution_codeandsource_user_id.
The ReportUtil class writes CSV error reports:
- File pattern:
{report_file_folder}/{ReportName}_log_{yyyyMMdd}.csv - Columns:
Barcode, Institution, Message - Reports are appended throughout the day.
A separate process (SchedulerMain) runs as a Heroku worker:
- Uses Quartz scheduler.
- Daily at 00:10 UTC: uploads the previous day's log file to FTP.
- File source:
logs/application.log_{yyyy-MM-dd}.log - If not found, falls back to current
logs/application.log.
When processing requests, if only a library name is available (not code):
- Call
GET /almaws/v1/conf/librariesfor the institution. - Build a name→code map (cached in memory per institution).
- Look up the code by name.
- If all resolution fails, use the institution's
default_libraryfrom config.
- Webhook messages are processed in separate threads (one thread per message).
- Item file processing uses a fixed thread pool (configurable, default 4 threads).
mergeItemsWithSCFandsendRequestsToSCFare synchronized methods — only one instance runs at a time.- FTP
getFilesis synchronized — prevents concurrent file downloads.
- Items in remote storage in a member institution must have a mirror copy in the SCF with barcode suffix "X".
- When an item is created/moves into a remote-storage location → create it in the SCF and loan it to the institution's system user.
- When an item is updated → sync metadata to the SCF copy.
- When an item leaves a remote-storage location or is deleted → optionally delete from SCF (controlled by
ignore_delete_files). - When a patron requests an item → create a hold request in the SCF against the system user.
- When the SCF returns a loaned item → scan it in at the member institution to trigger fulfillment.
- When a request is canceled in the SCF → propagate cancellation to the originating institution.
- If the SCF item is already on loan when a new request arrives → cancel the source request immediately.
- Digitization requests create linked users in the SCF and create digitization-type requests; then cancel the original title-level request in the source institution.
| Dependency | Purpose |
|---|---|
| Apache Commons Net | FTP client |
| JSch | SFTP client |
| MARC4J | MARC21 record parsing/writing |
| org.json | JSON parsing |
| jarchivelib | tar.gz extraction |
| Apache Commons IO | File utilities |
| Quartz | Job scheduling (log backup) |
| Log4j 1.2 | Logging |
| javax.servlet | HTTP servlet container |
| JAXB | XML binding (minimal usage) |
- Packaged as a WAR file, run with
webapp-runner.jar(embedded Tomcat). - Port from environment variable
$PORT. - Configuration via environment variable
CONFIG_FILEpointing to FTP URL of conf.json. - Two processes:
web(the servlet app) andscheduler(log backup cron job).