Configuration
The underwrite platform is configuration-driven. A single JSON file controls
which services are enabled, how they connect to infrastructure (bus, store,
identity), and service-specific parameters.
1. Configuration Loading
At startup, Configuration.load() searches for a configuration file
in the following order:
- Explicit path — if a path argument is provided and the file exists,
it is loaded immediately.
UNDERWRITE_ENV — if set to e.g. production, the loader tries
config.production.json.
Configuration.default() — if no file is found, a sensible default
configuration is used.
After file loading, __apply_env_overrides() overlays any matching
UNDERWRITE_* environment variables on top (see
ENVIRONMENT_VARIABLES.md). Values that
fail to parse (e.g. UNDERWRITE_AUTHZ_ENABLED=garbage) are logged
and the default is left in place — features are never silently
disabled by a bad env var.
The data_dir field is validated against a deny-list of sensitive
system paths (/etc, /proc, /sys, /var, /usr) to prevent
a misconfigured Configuration.data_dir=/etc from clobbering
system files.
Secret Redaction on Save
Configuration.to_dict() redacts every secret-shaped field across
every config section before the dict is written to disk by
Configuration.save(). The redaction list covers
key_secret, webhook_secret, api_token, token,
private_key, encryption_passphrase, cibil_api_key,
experian_api_key, equifax_api_key, and ckyc_api_key. There
is no path through the public API that persists these values
in plaintext.
config = Configuration.load("underwrite.json")
config.save("audit-only.json") # secrets are redacted
2. Pydantic Schema
Configuration extends ForbidExtra (which sets
model_config = {"extra": "forbid"}), so unknown keys in a JSON file cause a
ConfigurationError.
Top-level fields
| Field |
Type |
Default |
Description |
bus |
BusConfig |
BusConfig() |
Event bus settings |
store |
StoreConfig |
StoreConfig() |
Persistence backend |
logging |
LoggingConfig |
LoggingConfig() |
Log level/format/output |
identity |
IdentityConfig |
IdentityConfig() |
Ed25519 key material |
authz |
AuthzConfig |
AuthzConfig() |
Access control |
metrics |
MetricsConfig |
MetricsConfig() |
Metrics collection |
migration |
MigrationConfig |
MigrationConfig() |
Schema migration |
tracing |
TracingConfig |
TracingConfig() |
Distributed tracing |
saga |
SagaConfig |
SagaConfig() |
Saga orchestration |
secrets |
SecretsConfig |
SecretsConfig() |
Secrets backend configuration |
recovery |
RecoveryConfig |
RecoveryConfig() |
Auto-recovery settings |
fee |
FeeConfig |
FeeConfig() |
Fee schedules |
governance |
GovernanceConfig |
GovernanceConfig() |
Protocol governance parameters |
audit |
AuditConfig |
AuditConfig() |
Audit ledger limits |
kfs |
KfsConfig |
KfsConfig() |
Key Fact Statement config (RBI DLG) |
npa |
NpaConfig |
NpaConfig() |
NPA classification and provisioning (RBI) |
dpdpa |
DpdpaConfig |
DpdpaConfig() |
DPDPA 2023 data protection (India) |
razorpay |
RazorpayConfig |
RazorpayConfig() |
Razorpay payment gateway (India) |
credit_bureau |
CreditBureauConfig |
CreditBureauConfig() |
CIBIL/Experian/Equifax + CKYC |
underwriting |
UnderwritingConfig |
UnderwritingConfig() |
Underwriting rules and thresholds |
data_dir |
str |
"./data" |
Filesystem store data directory |
services |
dict[str, ServiceConfig] |
{} |
Per-service enablement/priority |
BusConfig
| Field |
Type |
Default |
Valid values |
backend |
str |
"local" |
local, sqs, modal |
rate_limit |
float |
0.0 |
>= 0 (0 = unlimited) |
max_workers |
int |
0 |
>= 0 (0 = synchronous dispatch) |
max_futures |
int |
10000 |
>= 1 |
StoreConfig
| Field |
Type |
Default |
Valid values |
backend |
str |
"sqlite" |
sqlite, memory |
path |
str |
"./store.db" |
SQLite path. Use :memory: for an in-process database. |
busy_timeout |
float |
30.0 |
Seconds. >= 0. |
LoggingConfig
| Field |
Type |
Default |
Valid values |
level |
str |
"INFO" |
DEBUG, INFO, WARNING, ERROR, CRITICAL |
output |
str |
"stdout" |
Free-form (e.g. stdout, stderr, file path) |
log_format |
str |
"text" |
text, json |
IdentityConfig
| Field |
Type |
Default |
private_key |
str |
"" |
public_key |
str |
"" |
encryption_passphrase |
str |
"" |
key_ttl |
float |
86400.0 |
key_grace |
float |
3600.0 |
AuthzConfig
| Field |
Type |
Default |
enabled |
bool |
true |
policy_file |
str |
"" |
MetricsConfig
| Field |
Type |
Default |
enabled |
bool |
true |
export_interval |
int |
60 |
TracingConfig
| Field |
Type |
Default |
Valid values |
enabled |
bool |
false |
|
exporter |
str |
"console" |
console, otlp, noop |
SecretsConfig
| Field |
Type |
Default |
backend |
str |
"env" |
url |
str |
"" |
token |
str |
"" |
region |
str |
"" |
RecoveryConfig
| Field |
Type |
Default |
auto_restart |
bool |
true |
max_restarts |
int |
3 |
backoff_seconds |
float |
1.0 |
KfsConfig (RBI Key Fact Statement)
| Field |
Type |
Default |
Description |
cooling_off_days |
int |
3 |
Free-look period per RBI DLG |
disclosure_version |
str |
"1.0" |
KFS template version |
NpaConfig (RBI Asset Classification)
| Field |
Type |
Default |
Description |
standard_provisioning_rate |
float |
0.0025 |
0.25% provisioning for standard assets |
substandard_provisioning_rate |
float |
0.15 |
15% for substandard assets |
doubtful_provisioning_rate_secured |
float |
0.25 |
25% for doubtful secured assets |
loss_provisioning_rate |
float |
1.0 |
100% for loss assets |
sma_0_days |
int |
30 |
SMA-0 threshold (30 DPD) |
sma_1_days |
int |
60 |
SMA-1 threshold (60 DPD) |
sma_2_days |
int |
90 |
SMA-2 threshold (90 DPD) |
npa_days |
int |
90 |
NPA classification at 90 DPD |
dlg_trigger_days |
int |
120 |
DLG trigger at 120+ DPD |
DpdpaConfig (DPDPA 2023)
ConsentConfig
| Field |
Type |
Default |
Description |
required_purposes |
list[str] |
[kyc_verification, credit_bureau_reporting, loan_servicing, collection, communication_transactional] |
Purposes for which consent is required |
consent_validity_days |
int |
365 |
Consent validity period |
withdrawal_cooldown_days |
int |
0 |
Cooldown before re-consent after withdrawal |
DsrConfig
| Field |
Type |
Default |
Description |
response_time_days |
int |
30 |
DSR fulfillment timeline (DPDPA mandate) |
grievance_response_days |
int |
15 |
Grievance response timeline |
dpo_email |
str |
"" |
Data Protection Officer email |
dpo_phone |
str |
"" |
Data Protection Officer phone |
DpdpaConfig top-level fields:
| Field |
Type |
Default |
Description |
consent |
ConsentConfig |
ConsentConfig() |
Consent management |
dsr |
DsrConfig |
DsrConfig() |
Data Subject Rights |
data_retention_years |
int |
8 |
Data retention per IT Act |
kyc_retention_years |
int |
5 |
KYC retention per PMLA |
breach_notification_hours |
int |
72 |
Breach notification window |
enable_breach_detection |
bool |
true |
Enable breach detection |
enable_auto_purge |
bool |
false |
Auto-purge expired data |
RazorpayConfig (Payment Gateway)
| Field |
Type |
Default |
Description |
key_id |
str |
"" |
Razorpay API key ID |
key_secret |
str |
"" |
Razorpay API key secret |
webhook_secret |
str |
"" |
Webhook signing secret |
api_base_url |
str |
https://api.razorpay.com/v1 |
API base URL |
upi_autopay_enabled |
bool |
true |
Enable UPI Autopay |
enable_nach |
bool |
true |
Enable e-NACH mandates |
CreditBureauConfig (CIBIL / Experian / Equifax / CKYC)
| Field |
Type |
Default |
Description |
cibil_enabled |
bool |
true |
Enable CIBIL check |
cibil_api_key |
str |
"" |
CIBIL API key |
cibil_api_base |
str |
https://api.cibil.com/v1 |
CIBIL API base |
experian_enabled |
bool |
false |
Enable Experian check |
experian_api_key |
str |
"" |
Experian API key |
equifax_enabled |
bool |
false |
Enable Equifax check |
equifax_api_key |
str |
"" |
Equifax API key |
ckyc_enabled |
bool |
true |
Enable CKYC verification |
ckyc_api_key |
str |
"" |
CKYC API key |
ckyc_api_base |
str |
https://api.ckycindia.in/v1 |
CKYC API base |
timeout_seconds |
int |
30 |
HTTP timeout |
UnderwritingConfig
| Field |
Type |
Default |
Description |
max_default_probability |
float |
0.25 |
Max default probability for approval |
min_credit_score |
int |
650 |
Min CIBIL/credit score |
max_dti_ratio |
float |
0.5 |
Max debt-to-income ratio |
max_ltv_ratio |
float |
0.8 |
Max loan-to-value ratio |
max_principal |
float |
10_000_000 |
Max loan amount |
min_principal |
float |
1_000 |
Min loan amount |
max_tenor_months |
int |
360 |
Max loan tenure |
ServiceConfig
| Field |
Type |
Default |
enabled |
bool |
false |
priority |
int |
0 |
3. Environment Variable Overrides
Every configuration field can be overridden at runtime via
UNDERWRITE_<SECTION>_<FIELD> environment variables. The
__apply_env_overrides() method (config.py:402) iterates a hardcoded
mapping, coerces the string value to the target type, and updates the config
object.
Boolean coersion accepts 1, true, yes (case-insensitive) for True;
everything else is False.
See ENVIRONMENT_VARIABLES.md for the complete
alphabetical listing.
4. Validation
- Schema-level: Pydantic
field_validator decorators enforce constrained
values (e.g. bus.backend must be one of local, sqs, modal;
rate_limit must be >= 0; logging.level must match a known log level).
- File-level:
Configuration.__merge() (config.py:288) validates
every section against its Pydantic model via model_copy + re-instantiation.
Unknown top-level keys and unknown per-section fields raise
ConfigurationError with a descriptive message.
- Path traversal: The loader rejects file paths containing
...
5. Serialization
config.to_dict() # → dict (strips secrets.token and identity.private_key)
config.save("path") # → JSON file (creates parent directories)
to_dict() excludes None values and redacts sensitive fields (token,
private_key) so serialised output is safe to log or store.
6. Default Configuration
Configuration.default() (config.py:222) provides:
Configuration(
store=StoreConfig(backend="sqlite", path="./store.db"),
services={name: ServiceConfig(enabled=False) for name in HANDLER_NAMES},
)
All other sections use their Pydantic model defaults (rate_limit=0, max_workers=0,
logging level=INFO, metrics enabled, etc.).
Example configuration file (underwrite.json)
{
"bus": {
"rate_limit": 100.0,
"max_workers": 4,
"max_futures": 10000,
"max_buffer_size": 10000
},
"store": {
"backend": "sqlite",
"path": "./store.db"
},
"services": {
"mechanism": {"enabled": true},
"audit": {"enabled": true},
"risk": {"enabled": true}
}
}
Note: max_buffer_size is accepted by LocalBus.__init__() but is not
a field in BusConfig. Set it directly in JSON — it will be passed through
at bus construction time.
Available Services
The platform defines 34 nano services (HANDLER_MAP in underwrite/handler.py:21):
audit, collateral, collection, communication, compliance, consent,
credit_bureau, decision, disbursement, document, dsr, fee, fraud,
governance, graph, identity, kfs, mechanism, npa, notification,
origination, payment, prepayment, pricing, quote, razorpay,
recovery, reporting, risk, servicing, settlement, statement,
underwriter, workflow