ADR-046: Satellite-iDempiere REST API Integration
Status
Proposed
Date
2025-12-10
Deciders
- Cloudempiere AI Team
Context and Problem Statement
ADR-043 through ADR-045 define the Chat API Service architecture. A key decision remains: How should the Satellite Service persist data (metrics, audit, usage) to iDempiere?
Options Considered
- Direct Database Access - Satellite connects directly to iDempiere PostgreSQL
- REST API Integration - Satellite calls iDempiere REST API endpoints
- Message Queue - Satellite publishes to queue, iDempiere consumes
Current Assumption (ADR-044)
ADR-044 assumed direct database access via Panache entities, which has issues:
- Tight coupling to iDempiere database schema
- Security concerns - Satellite needs DB credentials
- Schema changes break Satellite
- No iDempiere business logic - bypasses triggers, validators
- Multi-tenant complexity - must handle AD_Client_ID manually
Decision Drivers
- Loose coupling - Satellite should not depend on iDempiere internals
- Security - Minimize credential exposure
- Flexibility - Support multiple deployment scenarios
- iDempiere integration - Leverage existing REST API infrastructure
- Async capability - High-volume metrics shouldn't block requests
- Offline resilience - Satellite should work if iDempiere is temporarily unavailable
Decision Outcome
Use iDempiere REST API as primary integration, with optional message queue for high-volume async operations.
Architecture
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ CHAT API SERVICE │
│ │
│ ┌────────────────────────────────────────────────────────────────────────────────┐ │
│ │ AI Processing Layer │ │
│ │ • Chat completions │ │
│ │ • Embeddings │ │
│ │ • Tool execution │ │
│ └──────────────────────────────────┬─────────────────────────────────────────────┘ │
│ │ │
│ Generates: metrics, audit, usage data │
│ │ │
│ ┌──────────────────────────────────▼─────────────────────────────────────────────┐ │
│ │ Integration Layer │ │
│ │ │ │
│ │ ┌─────────────────────┐ ┌─────────────────────┐ ┌───────────────────────┐ │ │
│ │ │ Sync REST Client │ │ Async Queue Client │ │ Local Buffer │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ • Provider config │ │ • Metrics batch │ │ • Offline cache │ │ │
│ │ │ • Budget check │ │ • Audit events │ │ • Retry queue │ │ │
│ │ │ • Auth validation │ │ • Usage streaming │ │ • Circuit breaker │ │ │
│ │ └──────────┬──────────┘ └──────────┬──────────┘ └───────────┬───────────┘ │ │
│ │ │ │ │ │ │
│ └─────────────┼────────────────────────┼─────────────────────────┼───────────────┘ │
│ │ │ │ │
└────────────────┼────────────────────────┼─────────────────────────┼─────────────────┘
│ │ │
│ HTTP/REST │ AMQP/SQS │ (local)
│ │ │
┌────────────────▼────────────────────────▼─────────────────────────┘
│
│ ┌─────────────────────────────────────────────────────────────────────────────────┐
│ │ iDEMPIERE SERVER │
│ │ │
│ │ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ │ REST API Layer │ │
│ │ │ │ │
│ │ │ Existing Endpoints: New Satellite Endpoints: │ │
│ │ │ ┌─────────────────────────┐ ┌─────────────────────────────────┐ │ │
│ │ │ │ GET /api/v1/models │ │ POST /api/v1/ai/metrics │ │ │
│ │ │ │ POST /api/v1/models │ │ POST /api/v1/ai/audit │ │ │
│ │ │ │ GET /api/v1/windows │ │ POST /api/v1/ai/usage │ │ │
│ │ │ │ POST /api/v1/processes │ │ GET /api/v1/ai/providers │ │ │
│ │ │ └─────────────────────────┘ │ GET /api/v1/ai/budgets │ │ │
│ │ │ │ POST /api/v1/ai/conversations │ │ │
│ │ │ └─────────────────────────────────┘ │ │
│ │ └────────────────────────────────────────────────────────────────────────────┘ │
│ │ │ │
│ │ ┌──────────────────────────────────▼──────────────────────────────────────────┐│
│ │ │ Queue Consumer (Optional) ││
│ │ │ ││
│ │ │ • Listens to satellite.metrics queue ││
│ │ │ • Batch inserts to AIG_UsageMetrics ││
│ │ │ • Handles backpressure ││
│ │ └──────────────────────────────────────────────────────────────────────────────┘│
│ │ │ │
│ │ ┌──────────────────────────────────▼──────────────────────────────────────────┐│
│ │ │ Business Logic Layer ││
│ │ │ ││
│ │ │ • MAIProvider - Provider configuration ││
│ │ │ • MAIUsageMetrics - Usage recording ││
│ │ │ • MAIBudget - Cost limits ││
│ │ │ • MAIQueryAudit - Query audit ││
│ │ └──────────────────────────────────────────────────────────────────────────────┘│
│ │ │ │
│ │ ┌──────────────────────────────────▼──────────────────────────────────────────┐│
│ │ │ Database Layer ││
│ │ │ ││
│ │ │ AIG_Provider │ AIG_UsageMetrics │ AIG_Budget │ AIG_QueryAudit ││
│ │ └──────────────────────────────────────────────────────────────────────────────┘│
│ │ │
│ └──────────────────────────────────────────────────────────────────────────────────┘
│
└─────────────────────────────────────────────────────────────────────────────────────
Integration Patterns
Pattern 1: Synchronous REST (Real-time, Low Volume)
Use for: Provider config, budget checks, authentication validation
Satellite iDempiere REST API
│ │
│ GET /api/v1/ai/providers/{id} │
│ ────────────────────────────────────>│
│ │
│ <────────────────────────────────────
│ { "provider": {...}, "budget": {...}}
│ │
│ GET /api/v1/ai/budgets/check │
│ ?client_id=1000000&amount=0.05 │
│ ────────────────────────────────────>│
│ │
│ <────────────────────────────────────
│ { "allowed": true, "remaining": 95.50 }
│ │
Pattern 2: Async REST (Batched, Medium Volume)
Use for: Metrics, audit logs (batched every N seconds or N records)
Satellite iDempiere REST API
│ │
│ [Accumulate metrics locally] │
│ │
│ POST /api/v1/ai/metrics/batch │
│ [ │
│ { "model": "claude-sonnet-4", │
│ "input_tokens": 150, │
│ "output_tokens": 89, │
│ "cost_usd": 0.0012, │
│ "timestamp": "..." }, │
│ { ... }, │
│ { ... } │
│ ] │
│ ────────────────────────────────────>│
│ │
│ <────────────────────────────────────
│ { "accepted": 50, "failed": 0 } │
│ │
Pattern 3: Message Queue (High Volume, Async)
Use for: Real-time streaming metrics, high-throughput scenarios
Satellite Message Queue iDempiere Consumer
│ │ │
│ Publish metrics │ │
│ ───────────────────────────>│ │
│ │ ─────────────────────────>│
│ Publish metrics │ │ Batch insert
│ ───────────────────────────>│ │ to DB
│ │ ─────────────────────────>│
│ Publish metrics │ │
│ ───────────────────────────>│ │
│ │ ─────────────────────────>│
│ │ │
Queue Options:
- AWS SQS (cloud deployments)
- RabbitMQ (on-premise)
- Apache Kafka (high-throughput)
- Redis Streams (simple, fast)
Pattern 4: GraphQL (Future Option)
Use for: Flexible queries, reduced over-fetching, subscriptions
Satellite iDempiere GraphQL API
│ │
│ POST /graphql │
│ query { │
│ provider(id: 1) { │
│ name │
│ endpoint │
│ budget { │
│ dailyLimit │
│ dailyUsed │
│ } │
│ } │
│ } │
│ ────────────────────────────────────>│
│ │
│ <────────────────────────────────────
│ { "data": { "provider": {...} } } │
│ │
Benefits:
- Single request for provider + budget (no N+1)
- Client specifies exactly what fields needed
- Subscriptions for real-time budget alerts
- Strong typing with schema
Pattern 5: WebSocket (Real-time Bidirectional)
Use for: Real-time streaming metrics, live budget updates, bidirectional communication
Satellite iDempiere WebSocket
│ │
│ WS CONNECT /ws/ai │
│ ════════════════════════════════════>│
│ │
│ Subscribe: budget.updates │
│ ────────────────────────────────────>│
│ │
│ Stream metrics (continuous) │
│ ────────────────────────────────────>│
│ ────────────────────────────────────>│
│ ────────────────────────────────────>│
│ │
│ <────────────────────────────────────
│ Budget alert: 80% used │
│ │
│ <────────────────────────────────────
│ Provider config changed │
│ │
Benefits:
- Real-time bidirectional communication
- Push notifications (budget alerts, config changes)
- Lower latency than polling
- Efficient for streaming metrics
- Connection multiplexing
Use Cases:
- Live token usage dashboard
- Budget threshold alerts
- Provider health notifications
- Real-time metrics streaming
Integration Options Comparison
| Option | Latency | Throughput | Complexity | Real-time | Use Case |
|---|---|---|---|---|---|
| REST (sync) | Medium | Low | Low | No | Config, budget checks |
| REST (batch) | Low | Medium | Low | No | Metrics batching |
| Message Queue | Very Low | Very High | Medium | No | High-volume metrics |
| GraphQL | Medium | Medium | Medium | Subscriptions | Flexible queries |
| WebSocket | Very Low | High | Medium | Yes | Real-time streaming |
Recommended Combination
┌─────────────────────────────────────────────────────────────────────────┐
│ INTEGRATION LAYER STRATEGY │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Phase 1 (MVP): │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ REST API (Sync + Batch) │ │
│ │ • GET /api/v1/ai/providers - Config fetch │ │
│ │ • GET /api/v1/ai/budgets/check - Budget validation │ │
│ │ • POST /api/v1/ai/metrics/batch - Batched metrics │ │
│ │ • POST /api/v1/ai/audit/batch - Batched audit │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ Phase 2 (Scale): │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ + Message Queue (AWS SQS / RabbitMQ) │ │
│ │ • High-volume metrics streaming │ │
│ │ • Decoupled processing │ │
│ │ • Guaranteed delivery │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ Phase 3 (Real-time): │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ + WebSocket │ │
│ │ • Real-time budget alerts │ │
│ │ • Live metrics dashboard │ │
│ │ • Push config updates │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ Phase 4 (Flexible): │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ + GraphQL │ │
│ │ • Complex queries with nested data │ │
│ │ • Schema-driven development │ │
│ │ • Subscriptions for real-time │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
New iDempiere REST API Endpoints
Provider Management
GET /api/v1/ai/providers
description: List active AI providers for client
query:
- client_id: int (required)
- type: string (optional, e.g., "SAT", "ANT")
response:
- providers: array of provider configs
GET /api/v1/ai/providers/{id}
description: Get provider configuration
response:
- provider: { id, type, name, endpoint, model_name, ... }
- budget: { daily_limit, monthly_limit, current_usage }
POST /api/v1/ai/providers/{id}/health
description: Report provider health status
body:
- status: "UP" | "DOWN" | "DEGRADED"
- latency_ms: int
- error_message: string (optional)
Budget Management
GET /api/v1/ai/budgets/check
description: Check if request is within budget
query:
- client_id: int
- estimated_cost: decimal
response:
- allowed: boolean
- daily_remaining: decimal
- monthly_remaining: decimal
- warning: string (optional, e.g., "80% of daily budget used")
GET /api/v1/ai/budgets/{client_id}
description: Get budget configuration and current usage
response:
- daily_limit: decimal
- monthly_limit: decimal
- daily_used: decimal
- monthly_used: decimal
- alert_threshold: decimal
Metrics & Usage
POST /api/v1/ai/metrics
description: Record single metric (sync)
body:
- client_id: int
- user_id: int
- model: string
- input_tokens: int
- output_tokens: int
- cost_usd: decimal
- latency_ms: int
- session_id: string
- conversation_id: string
- success: boolean
- error_message: string (optional)
POST /api/v1/ai/metrics/batch
description: Record multiple metrics (batch)
body:
- metrics: array of metric objects
response:
- accepted: int
- failed: int
- errors: array (optional)
GET /api/v1/ai/metrics/summary
description: Get usage summary
query:
- client_id: int
- from: datetime
- to: datetime
- group_by: "day" | "model" | "user"
response:
- summary: aggregated metrics
Audit Logging
POST /api/v1/ai/audit
description: Record audit event
body:
- client_id: int
- user_id: int
- event_type: "CHAT" | "QUERY" | "TOOL" | "ERROR"
- model: string
- input_summary: string (truncated/hashed)
- output_summary: string (truncated/hashed)
- tokens: { input, output }
- metadata: object
POST /api/v1/ai/audit/batch
description: Record multiple audit events
body:
- events: array of audit objects
Conversation Management
POST /api/v1/ai/conversations
description: Create or update conversation
body:
- conversation_id: string
- client_id: int
- user_id: int
- title: string (optional, AI-generated)
- window_id: int (optional)
- record_id: int (optional)
GET /api/v1/ai/conversations/{id}
description: Get conversation details
response:
- conversation: { id, title, created, updated, message_count }
GET /api/v1/ai/conversations/{id}/messages
description: Get conversation messages (for context reload)
query:
- limit: int (default 50)
response:
- messages: array of { role, content, timestamp }
Satellite Integration Layer
REST Client Service
@ApplicationScoped
public class IdempiereRestClient {
@ConfigProperty(name = "idempiere.api.base-url")
String baseUrl;
@ConfigProperty(name = "idempiere.api.token")
String apiToken;
@Inject
@RestClient
IdempiereAiApi aiApi;
/**
* Get provider configuration (cached).
*/
@CacheResult(cacheName = "providers")
public ProviderConfig getProvider(int providerId) {
return aiApi.getProvider(providerId);
}
/**
* Check budget before processing request.
*/
public BudgetCheckResult checkBudget(int clientId, BigDecimal estimatedCost) {
return aiApi.checkBudget(clientId, estimatedCost);
}
/**
* Record metrics (async, batched).
*/
@Asynchronous
public void recordMetrics(UsageMetrics metrics) {
metricsBuffer.add(metrics);
if (metricsBuffer.size() >= BATCH_SIZE || shouldFlush()) {
flushMetrics();
}
}
/**
* Flush buffered metrics to iDempiere.
*/
@Scheduled(every = "10s")
void flushMetrics() {
if (metricsBuffer.isEmpty()) return;
List<UsageMetrics> batch = metricsBuffer.drain();
try {
aiApi.recordMetricsBatch(batch);
} catch (Exception e) {
// Add to retry queue
retryQueue.addAll(batch);
log.warn("Failed to flush metrics, queued for retry", e);
}
}
}
REST Client Interface
@Path("/api/v1/ai")
@RegisterRestClient(configKey = "idempiere-api")
public interface IdempiereAiApi {
@GET
@Path("/providers/{id}")
ProviderConfig getProvider(@PathParam("id") int providerId);
@GET
@Path("/budgets/check")
BudgetCheckResult checkBudget(
@QueryParam("client_id") int clientId,
@QueryParam("estimated_cost") BigDecimal cost);
@POST
@Path("/metrics/batch")
BatchResult recordMetricsBatch(List<UsageMetrics> metrics);
@POST
@Path("/audit/batch")
BatchResult recordAuditBatch(List<AuditEvent> events);
}
Queue Publisher (Optional)
@ApplicationScoped
public class MetricsQueuePublisher {
@Inject
@Channel("chat-api-metrics")
Emitter<UsageMetrics> metricsEmitter;
@ConfigProperty(name = "satellite.queue.enabled", defaultValue = "false")
boolean queueEnabled;
public void publish(UsageMetrics metrics) {
if (queueEnabled) {
metricsEmitter.send(metrics);
}
}
}
Configuration
Satellite application.properties
# ==================== iDempiere REST API Integration ====================
# Base URL of iDempiere REST API
idempiere.api.base-url=${IDEMPIERE_API_URL:http://localhost:8080}
# API authentication token
idempiere.api.token=${IDEMPIERE_API_TOKEN:}
# REST client configuration
quarkus.rest-client.idempiere-api.url=${idempiere.api.base-url}
quarkus.rest-client.idempiere-api.scope=singleton
# Connection pool
quarkus.rest-client.idempiere-api.connect-timeout=5000
quarkus.rest-client.idempiere-api.read-timeout=30000
# ==================== Metrics Integration ====================
# Integration mode: "rest" | "queue" | "both"
satellite.metrics.integration-mode=rest
# Batch settings (for REST mode)
satellite.metrics.batch-size=50
satellite.metrics.flush-interval=10s
# Retry settings
satellite.metrics.retry.max-attempts=3
satellite.metrics.retry.delay=5s
# Local buffer (for offline resilience)
satellite.metrics.buffer.max-size=10000
satellite.metrics.buffer.persist-on-shutdown=true
# ==================== Queue Integration (Optional) ====================
# Enable queue-based metrics
satellite.queue.enabled=false
satellite.queue.type=sqs
# AWS SQS
%sqs.quarkus.sqs.endpoint-override=${AWS_SQS_ENDPOINT:}
%sqs.quarkus.sqs.aws.region=${AWS_REGION:us-east-1}
satellite.queue.sqs.metrics-queue=${SQS_METRICS_QUEUE:chat-api-metrics}
# RabbitMQ
%rabbitmq.mp.messaging.outgoing.chat-api-metrics.connector=smallrye-rabbitmq
%rabbitmq.mp.messaging.outgoing.chat-api-metrics.queue.name=chat-api-metrics
# ==================== Cache Settings ====================
# Provider cache (from iDempiere)
quarkus.cache.caffeine.providers.expire-after-write=5m
quarkus.cache.caffeine.providers.maximum-size=100
# Budget cache (short TTL for accuracy)
quarkus.cache.caffeine.budgets.expire-after-write=30s
quarkus.cache.caffeine.budgets.maximum-size=50
Sequence Diagrams
Chat Request with REST Integration
┌──────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐
│ iDempiere │ │ Satellite │ │ iDempiere │ │ Claude │
│ Plugin │ │ Service │ │ REST API │ │ API │
└──────┬───────┘ └────────┬────────┘ └────────┬────────┘ └──────┬──────┘
│ │ │ │
│ POST /v1/chat/completions │ │
│ X-iDempiere-Client-ID: 1000000 │ │
│ ───────────────────>│ │ │
│ │ │ │
│ │ GET /api/v1/ai/budgets/check │
│ │ ?client_id=1000000&estimated_cost=0.05 │
│ │ ────────────────────>│ │
│ │ │ │
│ │ <──────────────────── │
│ │ { "allowed": true } │ │
│ │ │ │
│ │ [Process with Claude]│ │
│ │ ─────────────────────────────────────────>│
│ │ │ │
│ │ <─────────────────────────────────────────
│ │ │ │
│ <─────────────────── │ │
│ { "choices": [...] } │ │
│ │ │ │
│ │ [Async] POST /api/v1/ai/metrics │
│ │ { tokens, cost, latency } │
│ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─>│ │
│ │ │ │
High-Volume Metrics via Queue
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐
│ Satellite │ │ Message Queue │ │ iDempiere │ │ Database │
│ Service │ │ (SQS/RabbitMQ) │ │ Consumer │ │ │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘ └──────┬──────┘
│ │ │ │
│ Publish metric 1 │ │ │
│ ────────────────────>│ │ │
│ │ │ │
│ Publish metric 2 │ │ │
│ ────────────────────>│ │ │
│ │ │ │
│ Publish metric 3 │ [Batch consume] │ │
│ ────────────────────>│ ────────────────────>│ │
│ │ │ │
│ │ │ Batch INSERT │
│ │ │ ───────────────────>│
│ │ │ │
│ │ │ <───────────────────
│ │ │ Committed │
│ │ │ │
│ │ <──────────────────── │
│ │ ACK │ │
│ │ │ │
Comparison: Direct DB vs REST API
| Aspect | Direct Database | REST API |
|---|---|---|
| Coupling | Tight (schema dependent) | Loose (API contract) |
| Security | DB credentials needed | API token only |
| Business Logic | Bypassed | Enforced |
| Schema Changes | Breaking | Versioned API |
| Multi-tenant | Manual AD_Client_ID | Automatic via token |
| Offline Support | None | Local buffer + retry |
| Scalability | Limited by DB connections | Horizontal (API + queue) |
| Observability | DB metrics only | API metrics + tracing |
| Deployment | Same network required | Can be remote |
Migration Impact
Changes to ADR-044
Remove database entity layer from extraction:
- ### Layer 4: Database Access
-
- **Extraction: Replace iDempiere PO with Quarkus patterns**
-
- | iDempiere (X_/M*) | Quarkus (Panache/JPA) |
- |-------------------|----------------------|
- | `X_AIG_Provider` | `AigProvider` entity |
- | `MAIProvider` | `AigProviderRepository` |
+ ### Layer 4: REST API Integration
+
+ **New: REST client for iDempiere API**
+
+ | Data | REST Endpoint |
+ |------|---------------|
+ | Provider config | GET /api/v1/ai/providers/{id} |
+ | Budget check | GET /api/v1/ai/budgets/check |
+ | Metrics | POST /api/v1/ai/metrics/batch |
+ | Audit | POST /api/v1/ai/audit/batch |
Changes to ADR-045
Update package structure:
└── satellite/
- ├── model/
- │ ├── AigProvider.java # Provider config entity
- │ ├── AigUsageMetrics.java # Usage tracking entity
- │ └── AigProviderRepository.java # Panache repository
+ ├── client/
+ │ ├── IdempiereAiApi.java # REST client interface
+ │ ├── IdempiereRestClient.java # REST client service
+ │ └── dto/
+ │ ├── ProviderConfig.java # Provider response DTO
+ │ ├── BudgetCheckResult.java # Budget check response
+ │ └── BatchResult.java # Batch operation result
+ │
+ ├── queue/
+ │ ├── MetricsQueuePublisher.java # Queue publisher
+ │ └── MetricsBuffer.java # Local buffer
Consequences
Positive
- Loose coupling - Satellite doesn't know iDempiere schema
- Better security - No DB credentials in Satellite
- Business logic preserved - All writes go through iDempiere
- Offline resilience - Local buffering with retry
- Horizontal scaling - Queue enables high throughput
- API versioning - Non-breaking evolution
Negative
- Network dependency - Requires iDempiere to be reachable
- New API development - Need to add endpoints to iDempiere
- Latency - Additional HTTP hop for sync operations
- Complexity - Queue infrastructure (if used)
Risks & Mitigations
| Risk | Mitigation |
|---|---|
| iDempiere API unavailable | Local buffer + retry queue |
| High latency for budget checks | Aggressive caching (30s TTL) |
| Queue message loss | Persistent queues, acknowledgments |
| API rate limiting | Batching, exponential backoff |
Implementation Phases
Phase A: REST Client (Week 3-4)
- Create REST client interface
- Implement provider config fetching
- Implement budget checking
- Add caching layer
Phase B: Async Metrics (Week 4-5)
- Implement metrics buffering
- Add batch REST endpoint calls
- Implement retry logic
- Add circuit breaker
Phase C: Queue Integration (Optional, Week 6+)
- Add queue publisher
- Implement iDempiere consumer
- Configure queue infrastructure
- Performance testing
Related ADRs
- ADR-043: Satellite Service Architecture Evolution
- ADR-044: Component Extraction Strategy (updated)
- ADR-045: idempiere-cli Integration Architecture (updated)
ADR-046 | Version 1.0 | 2025-12-10 Status: Proposed