fluence-gateway-client
Ruby client for service-to-service communication through the Fluence API Gateway. Handles OAuth2 client_credentials authentication transparently, builds backend URLs from a service name, and forwards end-user tokens when needed.
| Piece | Role |
|---|---|
Fluence::Gateway::Client |
Thread-safe singleton exposing get / post / put / patch / delete. Token refresh, path building, and end-user token forwarding live here. |
Fluence::Gateway.configure |
Configuration DSL (credentials, gateway and appcenter URLs, OAuth2 scope, timeout, SSR flag, user-declared tenants). Every setting also accepts an ENV fallback. |
Fluence::Gateway::Client.with_service(:name) { } |
Thread-local scope that prefixes every path in the block with /backend/<service>/. |
Fluence::Gateway::Client.with_user_token(token) { } |
Thread-local scope that forwards an end-user Bearer token instead of the service token. |
Fluence::Gateway::Client.with_tenant(:name) { } |
Thread-local scope that routes every call in the block through a given tenant's gateway and credentials. Accepts built-in tenants (e.g. :mosaic) or user-declared ones from config.tenants. |
Fluence::Gateway::Error and subclasses |
AuthenticationError (OAuth2 failure), ConnectionError (gateway unreachable), RequestError (upstream 4xx/5xx), ServiceNotAllowedError (SSR tenant + service: kwarg), UnknownTenantError (unknown tenant name). |
Requires Ruby ≥ 3.2.
Installation
# Gemfile
source 'https://rubygems.pkg.github.com/fluence-eu' do
gem 'fluence-gateway-client'
end
bundle install
Configuration
Configure once at boot (Rails: config/initializers/fluence_gateway.rb).
Fluence::Gateway.configure do |c|
c.client_id = ENV.fetch('APPCENTER_CLIENT_ID')
c.client_secret = ENV.fetch('APPCENTER_CLIENT_SECRET')
end
Every setting resolves in three tiers: explicit setter value → matching ENV variable → hard-coded default. In CI or container deployments you can rely on the ENV layer and skip the configure block entirely, provided APPCENTER_CLIENT_ID and APPCENTER_CLIENT_SECRET are exported.
| Setting | Type | Default | ENV fallback | Behaviour |
|---|---|---|---|---|
client_id |
String | — | APPCENTER_CLIENT_ID |
OAuth2 client_credentials identifier. Required. |
client_secret |
String | — | APPCENTER_CLIENT_SECRET |
OAuth2 client_credentials secret. Required. |
gateway_url |
String | https://gateway.fluence.eu |
GATEWAY_URL |
Base URL of the API gateway. |
appcenter_url |
String | https://appcenter.fluence.eu |
APPCENTER_URL |
Base URL of the OAuth2 server (token endpoint). |
scope |
String | public |
APPCENTER_SCOPE |
OAuth2 scope requested on the client_credentials grant. |
ssr_tenant |
Boolean | false |
GATEWAY_SSR_TENANT (presence = enabled) |
When true, calls with a service: kwarg raise ServiceNotAllowedError. SSR tenants expose a single backend directly, so the /backend/<service>/ prefix has no meaning. |
ssl_verify |
Symbol / Boolean / String | :peer |
GATEWAY_SSL_VERIFY (see SSL peer verification) |
SSL peer verification mode for every HTTP call (token fetch + API). |
ssl_ca_file |
String | nil |
GATEWAY_SSL_CA_FILE |
Path to an additional CA certificate (PEM) that OpenSSL trusts on top of the system store (internal / self-signed CA). |
ssl_options |
Hash | {} |
— | Extra Faraday::SSLOptions keys merged last into the ssl: Hash. Keys here override anything derived from ssl_verify / ssl_ca_file. |
timeout |
Integer | 30 |
— | HTTP timeout, in seconds. |
tenants |
Hash | {} |
— | User-declared tenants (see Multi-tenant calls). |
profile |
Symbol | :production |
GATEWAY_PROFILE |
Profile of defaults used as the fallback layer (see Environment profiles). |
Only client_id and client_secret have no hard-coded default — they must come from either the setter or the ENV fallback before the first call.
The :mosaic built-in tenant's gateway_url can additionally be overridden through ENV['MOSAIC_URL'] (useful for staging).
Environment profiles
The profile setting swaps the fallback defaults of gateway_url, appcenter_url, ssl_verify, and the built-in tenants in one shot. Explicit setter values and matching ENV variables always win over profile defaults — the profile only affects what you get when nothing else is configured.
Available profiles:
| Profile | gateway_url |
appcenter_url |
ssl_verify |
:mosaic tenant gateway_url |
|---|---|---|---|---|
:production (default) |
https://gateway.fluence.eu |
https://appcenter.fluence.eu |
:peer |
https://mosaic.fluence.eu |
:local |
https://gateway.fluence-europe.dev |
https://appcenter.fluence-europe.dev |
:none |
https://mosaic.fluence-europe.dev |
The :local profile targets the Fluence local Caddy setup (*.fluence-europe.dev). SSL peer verification defaults to :none there because Ruby's OpenSSL trust store does not always pick up Caddy's self-signed Root CA — the setting stays overridable if you wire ssl_ca_file to the actual CA.
Fluence::Gateway.configure do |c|
c.client_id = ENV.fetch('APP_CENTER_CLIENT_ID')
c.client_secret = ENV.fetch('APP_CENTER_CLIENT_SECRET')
c.profile = Rails.env.production? ? :production : :local
end
Activating any non-:production profile emits a one-time warn on $stderr so the switch is never silent:
[Fluence::Gateway] profile=:local is active — using development defaults. Do not use in production.
Reading or writing a profile that is not a key of PROFILES raises ArgumentError eagerly.
SSL peer verification
ssl_verify controls SSL peer verification for every HTTP call (both the OAuth2 token fetch and API calls). The setting is propagated to the underlying Faraday::Connection via OAuth2::Client#connection_opts.
Accepted values:
Setter (config.ssl_verify = …) |
ENV (GATEWAY_SSL_VERIFY=…, case-insensitive) |
Canonical mode | |
|---|---|---|---|
true, :peer |
peer, true, 1, yes, on |
:peer — secure default (OpenSSL::SSL::VERIFY_PEER) |
|
false, :none |
none, false, 0, no, off |
:none — disabled (VERIFY_NONE, dev only) |
|
:client_once |
client_once |
`VERIFY_PEER \ | VERIFY_CLIENT_ONCE` |
:fail_if_no_peer_cert |
fail_if_no_peer_cert |
`VERIFY_PEER \ | VERIFY_FAIL_IF_NO_PEER_CERT` |
Any other value raises ArgumentError eagerly when the setting is read.
The first HTTP client built with ssl_verify: :none emits a one-time warn on $stderr so a disabled verification is never silent in logs:
[Fluence::Gateway] SSL peer verification is DISABLED (ssl_verify: :none). Do not use in production.
ssl_verify is also a supported per-tenant override (see Multi-tenant calls), so you can disable verification on a staging tenant while keeping it on for production.
Trusting a specific CA (ssl_ca_file)
When a gateway is signed by an internal or self-signed CA, the cleaner alternative to disabling verification is to point the client to the extra CA file:
Fluence::Gateway.configure do |c|
c.ssl_verify = :peer # keep verification on
c.ssl_ca_file = '/etc/ssl/internal-ca.pem'
end
Or via the ENV in CI:
export GATEWAY_SSL_CA_FILE=/etc/ssl/internal-ca.pem
Hosts signed by a system-trusted CA still validate against the system store as usual; the file configured here is loaded in addition to it.
Advanced: passing arbitrary Faraday SSL options (ssl_options)
For mTLS, pinned certificate stores, or any other Faraday::SSLOptions key, use the pass-through Hash:
Fluence::Gateway.configure do |c|
c. = {
client_cert: OpenSSL::X509::Certificate.new(File.read('/path/client.crt')),
client_key: OpenSSL::PKey::RSA.new(File.read('/path/client.key')),
verify_hostname: true
}
end
Keys in ssl_options win over those derived from ssl_verify and ssl_ca_file, so power users can override anything.
Usage
Calling a service
Pass the target service via the service: kwarg. The path is auto-prefixed with /backend/<service>/.
Fluence::Gateway::Client.get('/api/v1/valuations', service: :base_valeur)
# → GET /backend/base-valeur/api/v1/valuations
Fluence::Gateway::Client.post('/api/v1/valuations',
service: :base_valeur,
body: { asset_id: 42 })
Symbol service names have their underscores converted to dashes (:base_valeur → base-valeur). String service names are used as-is. When no service: is given, the path is sent to the gateway unchanged.
All verbs return parsed JSON (Hash or Array) for JSON content types, and the raw .body String for anything else (PDF, CSV, …). Pass raw: true to receive the full OAuth2::Response instead (useful for inspecting #headers or #status).
Scoping many calls to the same service
with_service stores the active service in the current thread, so every call inside the block inherits it:
Fluence::Gateway::Client.with_service(:base_valeur) do
Fluence::Gateway::Client.get('/api/v1/valuations')
Fluence::Gateway::Client.post('/api/v1/valuations', body: { asset_id: 42 })
end
A service: kwarg on the call site always overrides the block.
Forwarding an end-user token
When a request is made on behalf of an authenticated end-user, forward their Bearer so the gateway identifies the user instead of the calling service.
# Per call
Fluence::Gateway::Client.get('/api/v1/me',
service: :base_valeur,
user_token: current_user.access_token)
# Block — every call inside forwards the same token (thread-local)
Fluence::Gateway::Client.with_user_token(current_user.access_token) do
Fluence::Gateway::Client.get('/api/v1/me', service: :base_valeur)
Fluence::Gateway::Client.post('/api/v1/things',
service: :base_valeur,
body: { name: 'x' })
end
The service client_credentials token is cached separately: when a user_token: is present, it is wrapped in an ephemeral OAuth2::AccessToken for that request only. A user_token: kwarg always wins over a with_user_token block.
Multi-tenant calls
A single process can call several Fluence gateways (for example the main gateway and Mosaic) without re-configuring. Two sources of tenants are supported:
- Built-in tenants shipped with the gem — currently
:mosaic(SSR tenant athttps://mosaic.fluence.eu, overridable viaENV['MOSAIC_URL']). - User-declared tenants registered through
config.tenants.
Fluence::Gateway.configure do |c|
c.client_id = ENV.fetch('APPCENTER_CLIENT_ID')
c.client_secret = ENV.fetch('APPCENTER_CLIENT_SECRET')
c.tenants = {
internal: {
gateway_url: 'https://gateway.internal.fluence.eu',
client_id: ENV.fetch('INTERNAL_CLIENT_ID'),
client_secret: ENV.fetch('INTERNAL_CLIENT_SECRET')
}
}
end
Each tenant entry only needs the keys that differ from the global configuration; the rest is inherited. Supported override keys: :client_id, :client_secret, :gateway_url, :appcenter_url, :timeout, :scope, :ssr_tenant.
Route calls to a tenant per call or per block:
# Per call
Fluence::Gateway::Client.get('/api/v1/me', tenant: :mosaic)
# Block — every call inside is routed through the tenant (thread-local)
Fluence::Gateway::Client.with_tenant(:internal) do
Fluence::Gateway::Client.get('/api/v1/valuations', service: :base_valeur)
end
A tenant: kwarg on the call site always wins over with_tenant. Passing tenant: nil explicitly forces the global default tenant, even inside a with_tenant block. Unknown tenant names raise UnknownTenantError eagerly, before the block runs.
OAuth2 tokens are cached per tenant, so switching between tenants does not invalidate other tenants' tokens.
Error handling
begin
Fluence::Gateway::Client.get('/api/v1/me', tenant: :mosaic)
rescue Fluence::Gateway::AuthenticationError => e
# OAuth2 credentials invalid or token endpoint returned 4xx/5xx — do not retry
rescue Fluence::Gateway::ConnectionError => e
# Gateway unreachable (timeout, connection refused) — safe to retry with backoff
rescue Fluence::Gateway::RequestError => e
# Upstream service responded 4xx/5xx. e.status, e.body, e.headers are available
rescue Fluence::Gateway::ServiceNotAllowedError => e
# Tried to use a `service:` kwarg against a tenant with `ssr_tenant: true`
rescue Fluence::Gateway::UnknownTenantError => e
# `tenant:` referenced a name that is neither user-declared nor built-in
rescue Fluence::Gateway::Error => e
# Anything else raised by the gem
end
Token lifecycle
The service client_credentials token is fetched lazily on the first call and cached in memory. When it expires, the client:
- Calls
refreshon the cached token if a refresh token is available; - Otherwise, requests a fresh token via
client_credentials.
Refresh is guarded by a reentrant Monitor, so concurrent callers share a single refresh round-trip and reentrant paths (e.g. access_token_for → oauth_client_for) do not deadlock. OAuth2 tokens and clients are cached per tenant.
Development
bin/setup
bundle exec fluence-ci all # full catalog (lint + security + codequality + docs + tests)
bundle exec fluence-ci lint # rubocop only
bundle exec fluence-ci tests # rspec only
bundle exec fluence-ci docs --report # yardstick threshold check
bundle exec fluence-ci docs --build # generate YARD HTML in doc/
Contributing guidelines are in CONTRIBUTING.md.