The SAP connector is a Java 21 Spring Boot application that bridges HTTP requests from the Cloudflare Worker to live SAP systems via SAP Java Connector (JCo). Each SAP system (ECC, S/4HANA) gets its own connector instance.
Architecture
Cloudflare Worker
→ POST /api/sap/read-table (JSON)
→ SecurityFilter (X-Connector-Key)
→ ReadTableController
→ ReadTableService
→ JCo → RFC_READ_TABLE → SAP
Every request follows the same pattern: controller validates input, service calls JCo, response wraps in ConnectorResponse.
Directory structure
connector/
├── pom.xml # Maven build (Java 21, Spring Boot 3)
├── Dockerfile # Production Docker image
├── lib/ # SAP JCo libraries (not in repo)
│ ├── sapjco3.jar # Java API
│ └── libsapjco3.so # Native library (Linux)
├── src/main/
│ ├── resources/
│ │ └── application.yml # Spring Boot config
│ └── java/com/aisi/connector/
│ ├── ConnectorApplication.java
│ ├── config/
│ │ ├── JCoConfig.java # JCo destination provider registration
│ │ ├── DynamicDestinationProvider.java # Static + per-request SAP destinations
│ │ ├── SapProperties.java # @ConfigurationProperties
│ │ ├── SecurityFilter.java # API key auth
│ │ └── SapUserFilter.java # Optional per-request SAP user/password headers
│ ├── controller/ # REST endpoints
│ ├── service/ # JCo business logic
│ ├── mock/ # Mock services (no SAP needed)
│ ├── model/ # ConnectorResponse, SapError
│ └── exception/ # GlobalExceptionHandler
Configuration
application.yml
server:
port: 8080
sap:
mock-mode: ${SAP_MOCK_MODE:false}
host: ${SAP_HOST:}
system-number: ${SAP_SYSTEM_NUMBER:00}
client: ${SAP_CLIENT:100}
user: ${SAP_USER:}
password: ${SAP_PASSWORD:}
language: ${SAP_LANGUAGE:EN}
router-string: ${SAP_ROUTER_STRING:}
pool-capacity: ${SAP_POOL_CAPACITY:5}
connector:
api-key: ${CONNECTOR_API_KEY:}
All values can be overridden via environment variables.
Configuration properties
SapProperties.java binds the sap.* properties:
| Property | Default | Description |
|---|
sap.mock-mode | false | Skip JCo, use mock services |
sap.host | — | SAP application server hostname |
sap.system-number | 00 | System number (00–99) |
sap.client | 100 | Mandant/client number |
sap.user | — | Default/fallback SAP logon user (optional when using per-request headers) |
sap.password | — | Default/fallback SAP logon password (optional when using per-request headers) |
sap.language | EN | SAP logon language |
sap.router-string | — | SAProuter string (for network routing) |
sap.pool-capacity | 5 | JCo connection pool size |
API endpoints
Most endpoints are POST /api/sap/* (with a few GET endpoints like probe-changes and read-source) and return a standard response wrapper:
{
"success": true,
"data": { ... }
}
On error:
{
"success": false,
"error": "Error message"
}
Health check
Returns { status: "ok", mockMode: true/false }. No authentication required.
read-table
Reads rows from an SAP table via RFC_READ_TABLE.
| Param | Type | Description |
|---|
table_name | string | SAP table name (e.g., MARA) |
fields | string[] | Fields to return (optional — all if omitted) |
filters | string[] | WHERE clauses (e.g., ["MTART = 'FERT'"]) |
limit | number | Max rows (default 100) |
Response data: { table, row_count, sample_data: [{ field: value }] }
table-schema
POST /api/sap/table-schema
Returns DDIC metadata for a table (field names, types, lengths, key flags, descriptions).
| Param | Type | Description |
|---|
table_name | string | SAP table name |
Response data: { table, field_count, fields: [{ name, type, length, key, description }] }
search-abap
POST /api/sap/search-abap
Searches for ABAP objects by name in TADIR.
| Param | Type | Description |
|---|
search_string | string | Search term |
object_types | string[] | Filter by object type (optional) |
Response data: { matches: [{ object_type, object_name, line, snippet }] }
The search_abap_source tool post-processes this response: it deduplicates matches by object name, strips line and snippet fields, and returns { search_pattern, object_types, match_count, matches: [{ object_type, object_name }] }.
analyze-code
POST /api/sap/analyze-code
Analyzes ABAP source for S/4HANA compatibility issues.
| Param | Type | Description |
|---|
program_name | string | ABAP program name |
analysis_type | string | Analysis type (optional) |
Response data: { findings: [{ severity, line, message, recommendation }] }
The analyze_custom_code tool accepts object_name (which can be a program, function module, function group, or class). It resolves the program name before sending program_name to the connector.
apply-code-fix
POST /api/sap/apply-code-fix
Modifies ABAP program source via Editor Lock APIs.
| Param | Type | Description |
|---|
program | string | Program name |
changes | array | [{ line, old_code, new_code }] |
transport | string | Transport request (optional) |
Response data: { success, changes_applied }
create-transport
POST /api/sap/create-transport
Creates a transport request via BAPI_CTREQUEST_CREATE.
| Param | Type | Description |
|---|
description | string | Transport description |
target_system | string | Target system ID |
objects | array | Objects to include (optional) |
Response data: { transport_id, status }
execute-rfc
POST /api/sap/execute-rfc
Executes an arbitrary RFC function module.
| Param | Type | Description |
|---|
function_module | string | RFC function name |
import_params | object | Import parameters (optional) |
tables_params | object | Table parameters (optional) |
Response data: { export_params, return_tables, parsed_rows }
simulate-rfc
POST /api/sap/simulate-rfc
Dry-run validation of an RFC call — checks parameters without executing.
| Param | Type | Description |
|---|
function_module | string | RFC function name |
import_params | object | Import parameters (optional) |
tables_params | object | Table parameters (optional) |
Response data: { validation_passed, messages }
execute-rfc-batch
POST /api/sap/execute-rfc-batch
Executes multiple RFC calls in a single stateful session (same database LUW). Used by the ETL extract_transform_load tool for batched writes with transactional commit.
| Param | Type | Description |
|---|
calls | array | [{ function_module, import_params, tables_params }] |
Response data: [{ function_module, success, message, export_params, return_tables }] (array of result objects)
transaction-details
POST /api/sap/transaction-details
Looks up a transaction code (TSTC table).
| Param | Type | Description |
|---|
tcode | string | Transaction code |
Response data: { tcode, program, description, module }
probe-changes
GET /api/sap/probe-changes?since=YYYYMMDD
Checks whether metadata has changed since the given date (used to skip no-op delta runs).
| Param | Type | Description |
|---|
since | string | YYYYMMDD date |
Response data: { tableChanges, codeChanges }
index-custom-objects
POST /api/sap/index-custom-objects
POST /api/sap/index-custom-objects?since=YYYYMMDD
Indexes custom tables, classes, programs, and related metadata.
index-relationships
POST /api/sap/index-relationships
Indexes foreign keys and cross-reference relationships.
index-catalog-context
POST /api/sap/index-catalog-context
Indexes catalog context (package and module mappings).
index-custom-code
POST /api/sap/index-custom-code
POST /api/sap/index-custom-code?since=YYYYMMDD
Indexes custom code metadata for analysis.
index-modifications
POST /api/sap/index-modifications
Indexes user exits and modification markers.
index-enhancements
POST /api/sap/index-enhancements
Indexes enhancement spots and implementations.
index-s4-extensions
POST /api/sap/index-s4-extensions
Indexes S/4HANA extension metadata (S/4-only).
read-source
GET /api/sap/read-source?name=OBJECT_NAME&type=program|class|bapi
Reads ABAP source code for a program, class, or BAPI/FM.
Response data: { object_name, object_type, source, line_count }
test-connection
POST /api/sap/test-connection
Tests connectivity and permissions. Returns system info, user info, roles, and capability test results.
Response data:
{
"system_info": { "host", "system_id", "client", "release", "database" },
"user_info": { "username", "ustyp", "uflag", "gltgb" },
"roles": ["ROLE_NAME", ...],
"capability_tests": {
"read_table": { "success": true },
"get_schema": { "success": true },
"read_dictionary": { "success": true },
"analyze_code": { "success": true }
}
}
See Connection Test Implementation for details.
JCo setup
Download
SAP JCo is proprietary and must be downloaded from the SAP Support Portal with S-user credentials. Search for “SAP JCo 3.1”.
You need two files:
| File | Platform | Purpose |
|---|
sapjco3.jar | All | Java API |
libsapjco3.so | Linux | Native library |
sapjco3.dll | Windows | Native library |
libsapjco3.jnilib | macOS | Native library |
Place both in connector/lib/.
Maven dependency
The JCo JAR uses a system scope in pom.xml since it’s not in Maven Central:
<dependency>
<groupId>com.sap.conn.jco</groupId>
<artifactId>sapjco3</artifactId>
<version>3.1</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/sapjco3.jar</systemPath>
</dependency>
Connection configuration
JCoConfig.java registers DynamicDestinationProvider, which supports:
- Static destination from
SapProperties (host/sysnr/client/language and optional fallback user/password)
- Per-request dynamic destination when
X-SAP-User and X-SAP-Password headers are provided
Core static properties:
Properties p = new Properties();
p.setProperty(JCO_ASHOST, props.getHost());
p.setProperty(JCO_SYSNR, props.getSystemNumber());
p.setProperty(JCO_CLIENT, props.getClient());
p.setProperty(JCO_LANG, props.getLanguage());
p.setProperty(JCO_POOL_CAPACITY, String.valueOf(props.getPoolCapacity()));
Per-request SAP credentials are applied by SapUserFilter:
- Reads
X-SAP-User and X-SAP-Password
- Registers a thread-local dynamic destination for that request
- Falls back to static destination when headers are absent
JCo manages its own connection pool internally. pool-capacity controls static destination pooling; per-request dynamic destinations use single-connection semantics.
JCoConfig is annotated with @ConditionalOnProperty(name = "sap.mock-mode", havingValue = "false"). When mock mode is enabled, no JCo destination is registered and no SAP connection is attempted.
Authentication
SecurityFilter.java implements a servlet filter:
/health endpoint — no auth required
- If
CONNECTOR_API_KEY is not set — all requests allowed (dev mode)
- Otherwise — requests must include
X-Connector-Key header matching the configured key
On startup, a @PostConstruct validation ensures that CONNECTOR_API_KEY is set when running in real mode (SAP_MOCK_MODE=false). If the key is missing, the connector fails immediately with an IllegalStateException rather than silently running without authentication.
The Cloudflare Worker sends connector auth via sap-connector-client.ts:
headers: {
'Content-Type': 'application/json',
'X-Connector-Key': connectorKey,
'X-SAP-User': sapUser, // optional
'X-SAP-Password': sapPassword // optional
}
In production, always set CONNECTOR_API_KEY on the connector and store the same value in that connection’s connection_secrets.connector_key in the Worker app. Without this, any HTTP client can call the connector directly.
Mock mode
Set SAP_MOCK_MODE=true to run without a real SAP connection. Each service has a corresponding mock implementation:
| Service | Mock |
|---|
ReadTableService | MockReadTableService |
TableSchemaService | MockTableSchemaService |
RfcService | MockRfcService |
| … | … |
Spring Boot’s @ConditionalOnProperty automatically injects the correct service based on the sap.mock-mode flag. Mock services return realistic fake data.
Mock mode is useful for:
- Local development without SAP access
- CI/CD test pipelines
- Demo environments
Docker deployment
Build
cd connector
mvn clean package -DskipTests
docker build --platform linux/amd64 -t sap-connector .
The --platform linux/amd64 flag is required on Apple Silicon Macs because the JCo native library (libsapjco3.so) is x86-only.
Dockerfile
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY target/connector-*.jar app.jar
COPY lib/sapjco3.jar /app/lib/sapjco3.jar
COPY lib/libsapjco3.so /app/lib/
ENV LD_LIBRARY_PATH=/app/lib
EXPOSE 8080
ENTRYPOINT ["java", "-Xmx1536m", "-Xms512m",
"-Dloader.path=/app/lib/sapjco3.jar",
"-jar", "app.jar"]
Run
docker run -p 8080:8080 \
-e SAP_HOST=sap.example.com \
-e SAP_SYSTEM_NUMBER=00 \
-e SAP_CLIENT=100 \
-e SAP_USER=your_user \
-e SAP_PASSWORD=your_password \
-e CONNECTOR_API_KEY=secret123 \
sap-connector
Dual-system deployment
For dual-system support (ECC + S/4HANA), run two connector instances pointing to different SAP systems:
# S/4HANA connector on port 8080
docker run -d --name sap-connector \
-p 8080:8080 \
-e SAP_HOST=s4hana.example.com \
-e SAP_CLIENT=100 \
-e CONNECTOR_API_KEY=secret123 \
sap-connector
# ECC connector on port 8081 (remap container's 8080 → host 8081)
docker run -d --name sap-connector-ecc \
-p 8081:8080 \
-e SAP_HOST=ecc.example.com \
-e SAP_CLIENT=800 \
-e CONNECTOR_API_KEY=secret123 \
sap-connector
Both containers use the same Docker image. The second container remaps port 8080 inside the container to port 8081 on the host via -p 8081:8080 — no need to change SERVER_PORT.
Then configure both URLs as separate workspace_connections entries for the workspace.
The Worker routes each SAP tool call to the correct connector using the tool input connection_id selected from workspace_connections (not by ecc_* / s4_* tool name prefixes).
Local development
Dev SAP systems
The project uses two hosted SAP training systems:
| System | ID | Description | Host |
|---|
| S/4HANA | S23 | S/4HANA 2023 FAA FPS00 | 172.21.72.22 |
| ECC | IDE | SAP ECC 6.0 IDES | 172.21.72.16 |
Both are hosted by Michael Management / 1stBasis. If credentials expire or get locked, reset them at https://mmc-solman01.mmc.1stbasis.com:50101/m2bdiyadm# then recreate the container with the new password.
Building and running locally
cd connector
mvn clean package -DskipTests
docker build --platform linux/amd64 -t sap-connector .
S/4HANA connector (port 8080):
docker run -d --name sap-connector \
-p 8080:8080 \
-e SAP_SYSTEM_NUMBER=00 \
-e SAP_CLIENT=100 \
-e SAP_USER=STUDENT185 \
-e "SAP_PASSWORD=Password1234" \
-e SAP_LANGUAGE=EN \
-e SAP_ROUTER_STRING=/H/161.38.17.212 \
-e SAP_HOST=172.21.72.22 \
-e CONNECTOR_API_KEY=aisi-connector-dev-key \
sap-connector
ECC connector (port 8081):
docker run -d --name sap-connector-ecc \
-p 8081:8080 \
-e SAP_SYSTEM_NUMBER=01 \
-e SAP_CLIENT=800 \
-e SAP_USER=STUDENT008 \
-e "SAP_PASSWORD=CA480nIv" \
-e SAP_LANGUAGE=EN \
-e SAP_ROUTER_STRING=/H/161.38.17.212 \
-e SAP_HOST=172.21.72.16 \
-e CONNECTOR_API_KEY=aisi-connector-dev-key \
sap-connector
Health checks:
# S/4HANA — should return {"status":"ok","mockMode":false}
curl -s http://localhost:8080/health -H "X-Connector-Key: aisi-connector-dev-key"
# ECC — should return {"status":"ok","mockMode":false}
curl -s http://localhost:8081/health -H "X-Connector-Key: aisi-connector-dev-key"
# Quick status check (both containers running)
docker ps --filter name=sap-connector --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
Rebuilding after code changes
docker stop sap-connector sap-connector-ecc
docker rm sap-connector sap-connector-ecc
cd connector && mvn clean package -DskipTests && docker build --platform linux/amd64 -t sap-connector .
# Then re-run the docker run commands above
CONNECTOR_API_KEY must match that connection’s connection_secrets.connector_key (seeded as aisi-connector-dev-key locally). By default the connector runs in real mode (connects to SAP via JCo). Only add -e SAP_MOCK_MODE=true if you explicitly need mock mode.