bridgex
v2.2.0
Published
a library for mazz app or a bridge for messaging that allow and automate the use of our service
Readme
SMS Client SDK
A fault-tolerant TypeScript SDK for sending SMS messages — usable as a library, a standalone microservice, or a background job processor.
Table of Contents
- Installation
- Quick Start
- Auto-Provisioning Credentials
- SendConfig — Fluent Parameter Builder
- Send Methods
- Result & Error Types
- SMSQueue — Background Job Queue
- SMSScheduler — Recurring & Scheduled Sends
- SMSService — Run as a Microservice
- Plugins & Hooks
- Fault Tolerance (Retry + Circuit Breaker)
- Full Configuration Reference
- File Structure
Installation
npm install bridgexQuick Start
import { SMSClient, SendConfig, LoggerPlugin } from "bridgex";
const client = new SMSClient({
baseUrl: "https://api.your-service.com",
apiKey: "your-access-token",
projectKey: "your-repo-token",
});
client.use(LoggerPlugin("my-app"));
const result = await client.send({
to: "+15551234567",
template: "Hello {name}, your order {orderId} is confirmed!",
variables: { name: "Alice", orderId: "ORD-999" },
});
if (result.ok) {
console.log("Sent!", result.data);
} else {
console.error(result.error); // typed ErrorLog — never throws
}Auto-Provisioning Credentials
Your service auto-generates a repo token, access token, and URL. Use CredentialProvisioner to fetch them automatically.
import { CredentialProvisioner, SMSClient } from "bridgex";
const credentials = await CredentialProvisioner.provision({
provisionUrl: "https://api.your-service.com/provision",
adminKey: "your-admin-key",
projectName: "my-app",
});
// credentials = { baseUrl, apiKey, projectKey }
const client = new SMSClient(credentials);Accepts camelCase or snake_case field names from your API (apiKey, api_key, accessToken, access_token, etc.).
SendConfig — Fluent Parameter Builder
SendConfig lets you define templates, defaults, tags, TTLs, and priority once and reuse them across many calls. No more copying the same template string everywhere.
import { SendConfig } from "bridgex";
// Build a reusable config
const otpConfig = SendConfig.otp(10) // preset: OTP with 10-min expiry
.set("appName", "MyApp"); // add a shared default variable
// Use it
const result = await client.send(
otpConfig.for("+15551234567", { code: "482910" }),
);
// Send to many recipients with per-recipient variables
const results = await client.sendMany(
otpConfig.forMany([
{ to: "+15550000001", variables: { code: "111111" } },
{ to: "+15550000002", variables: { code: "222222" } },
]),
otpConfig._template, // or pass template inline
);Builder Methods
| Method | Description |
| ------------------------------------ | ---------------------------------------------- |
| .template(str) | Set the message template |
| .defaults(vars) | Default variable values (overridable per-call) |
| .set(key, value) | Add a single default variable |
| .tag(...tags) | Label this config (used in logs and hooks) |
| .ttl(ms) | Drop jobs older than this (queue only) |
| .priority("high"\|"normal"\|"low") | Queue priority |
| .dedupKey(key) | Suppress duplicate jobs in the queue |
| .clone() | Copy without mutating |
| .merge(other) | Combine two configs (other wins on conflicts) |
Static Presets
SendConfig.otp(expiryMinutes?) // OTP — high priority, TTL-aware
SendConfig.orderNotification() // Order status updates
SendConfig.reminder() // Appointment / task reminders
SendConfig.promo() // Marketing messages — low priorityMaterialise Methods
config.for(to, variables?) // → SendParams (use with client.send)
config.forMany(recipients) // → SendParams[] (use with client.sendMany)
config.forObject(to, object) // → SendObjectParams (use with client.sendObject)Send Methods
All methods return a Result and never throw.
client.send(params) → Promise<Result>
const result = await client.send({
to: "+15551234567",
template: "Hi {user.firstName}, your balance is {account.balance}",
variables: {
user: { firstName: "Bob" },
account: { balance: "$42.00" },
},
});Supports nested variable paths ({nested.key}).
client.sendMany(recipients, template, sharedVars?) → Promise<BatchResult>
const result = await client.sendMany(
[
{ to: "+15550000001", variables: { name: "Alice" } },
{ to: "+15550000002", variables: { name: "Bob" } },
],
"Hi {name}, your appointment is tomorrow at {time}.",
{ time: "10:00 AM" },
);
console.log(`${result.successCount}/${result.total} sent`);
result.failed.forEach((f) => console.error(`[${f.to}]`, f.error.message));client.sendObject(params) → Promise<Result>
Auto-extracts the message from message, body, text, or content fields. Or pass a template to use any field as a variable.
// Auto-detection
await client.sendObject({
to: "+15551234567",
object: { message: "Your package has shipped!", trackingId: "TRK-123" },
});
// With template
await client.sendObject({
to: "+15551234567",
object: { firstName: "Alice", trackingId: "TRK-123" },
template: "Hi {firstName}, your tracking ID is {trackingId}.",
});client.sendObjectMany(items) → Promise<BatchResult>
await client.sendObjectMany([
{
to: "+15550000001",
object: { firstName: "Alice", orderId: "ORD-1" },
template: "Hi {firstName}, order {orderId} is ready.",
},
{
to: "+15550000002",
object: { firstName: "Bob", orderId: "ORD-2" },
template: "Hi {firstName}, order {orderId} is ready.",
},
]);Result & Error Types
type Result<T> =
| { ok: true; data: T; error: null }
| { ok: false; data: null; error: ErrorLog };
interface ErrorLog {
name: string;
message: string;
code: string; // see Error Codes table
isClientError: boolean; // your code caused it
isServerError: boolean; // server returned an error
details?: unknown;
timestamp: string; // ISO 8601
}BatchResult
interface BatchResult<T> {
succeeded: Array<{ index: number; to: string; data: T }>;
failed: Array<{ index: number; to: string; error: ErrorLog }>;
total: number;
successCount: number;
failureCount: number;
}Error Codes
| Code | Client? | Server? | Cause |
| ------------------ | ------- | ------- | ------------------------------ |
| VALIDATION_ERROR | ✅ | ❌ | Missing required param |
| TEMPLATE_ERROR | ✅ | ❌ | Variable missing from template |
| SERVER_ERROR | ❌ | ✅ | Non-2xx HTTP response |
| RATE_LIMIT_ERROR | ❌ | ✅ | 429 — respects Retry-After |
| NETWORK_ERROR | ❌ | ❌ | Timeout / no connectivity |
| CIRCUIT_OPEN | ❌ | ❌ | Circuit breaker tripped |
SMSQueue — Background Job Queue
Fire-and-forget sending with priority lanes, deduplication, TTL, and delayed sends.
import { SMSQueue, SendConfig, LoggerPlugin, PluginManager } from "bridgex";
const plugins = new PluginManager().use(LoggerPlugin("worker"));
const queue = new SMSQueue(client, { concurrency: 5 }, plugins);
queue.start();
// Immediate
const otpCfg = SendConfig.otp().dedupKey(`otp:${userId}`);
queue.enqueue(otpCfg.for(phone, { code: "482910" }));
// Delayed (send in 5 minutes)
queue.enqueueAfter(reminderCfg.for(phone), 5 * 60 * 1000);
// At a specific time
queue.enqueueAt(promoCfg.for(phone), new Date("2025-12-25T09:00:00").getTime());
// Stats
console.log(queue.stats());
// { pending: 3, running: 1, completed: 47, dropped: 2 }
// Cancel a specific job
queue.cancel(jobId);
// Drain all remaining jobs then stop
await queue.drain();Queue Options
| Option | Default | Description |
| -------------- | ------- | -------------------------------------------- |
| concurrency | 3 | Max parallel workers |
| pollInterval | 1000 | How often (ms) to check for ready jobs |
| autoStart | true | Start workers automatically on first enqueue |
SMSScheduler — Recurring & Scheduled Sends
import { SMSScheduler, SendConfig } from "bridgex";
const scheduler = new SMSScheduler(queue);
scheduler.start();
// Every 24 hours
scheduler.every(
24 * 60 * 60 * 1000,
"daily-report",
reportConfig.for(adminPhone),
);
// Cron expression: weekdays at 9am
scheduler.cron("0 9 * * 1-5", "weekday-digest", digestConfig.for(adminPhone), {
maxRuns: 50,
});
// One-shot future send
scheduler.once(
new Date("2025-06-01T09:00:00"),
"summer-launch",
promoConfig.for(phone),
);
// Control
scheduler.pause("daily-report");
scheduler.resume("daily-report");
scheduler.remove("summer-launch");
// List all scheduled jobs
console.log(scheduler.list());Cron format: minute hour day-of-month month day-of-week
*= every,,= list,-= range,/= step- Example:
"30 8,12,18 * * *"= 8:30, 12:30, and 18:30 every day
SMSService — Run as a Microservice
Deploy the SDK as a standalone REST service so any other service (Python, Go, Ruby, etc.) can send SMS without bundling this SDK.
import {
SMSClient,
SMSQueue,
SMSService,
MetricsPlugin,
PluginManager,
} from "bridgex";
const client = new SMSClient({ baseUrl, apiKey, projectKey });
const queue = new SMSQueue(client, { concurrency: 10 });
const metrics = MetricsPlugin();
client.use(metrics);
queue.start();
const service = new SMSService(client, {
port: 4000,
apiKey: "my-service-secret",
})
.withQueue(queue)
.withMetrics(metrics);
await service.start();
// [SMSService] Listening on http://0.0.0.0:4000REST API
| Method | Path | Description |
| -------- | ------------------- | ------------------------------------ |
| POST | /send | Send a single message |
| POST | /send/many | Send to many recipients |
| POST | /send/object | Send from a plain object |
| POST | /send/object/many | Send objects to many recipients |
| POST | /queue/enqueue | Fire-and-forget via queue |
| POST | /queue/schedule | Schedule a future send |
| GET | /queue/stats | Queue stats |
| DELETE | /queue/:id | Cancel a queued job |
| GET | /health | Liveness + circuit state + uptime |
| GET | /metrics | Send rate, success rate, avg latency |
All requests must include x-service-key: <your-service-secret> if apiKey is configured.
Example: POST /send
curl -X POST http://localhost:4000/send \
-H "Content-Type: application/json" \
-H "x-service-key: my-service-secret" \
-d '{ "to": "+15551234567", "template": "Hello {name}", "variables": { "name": "Alice" } }'Example: POST /queue/schedule
# Send at a specific time
curl -X POST http://localhost:4000/queue/schedule \
-H "x-service-key: my-service-secret" \
-H "Content-Type: application/json" \
-d '{ "at": "2025-12-25T09:00:00Z", "to": "+1555...", "template": "Merry Christmas {name}!", "variables": { "name": "Bob" } }'
# Send after a delay
curl -X POST http://localhost:4000/queue/schedule \
-H "x-service-key: my-service-secret" \
-H "Content-Type: application/json" \
-d '{ "delayMs": 300000, "to": "+1555...", "template": "Your reminder: {text}", "variables": { "text": "Call the dentist" } }'GET /health response
{
"status": "ok",
"circuitState": "CLOSED",
"uptime": 3600,
"startedAt": "2025-01-01T09:00:00.000Z",
"queue": { "pending": 0, "running": 2, "completed": 1024, "dropped": 3 }
}Plugins & Hooks
Plugins run on every send lifecycle event. They're async and never crash the main flow.
import { SMSClient, LoggerPlugin, MetricsPlugin } from "bridgex";
// Built-in logger
client.use(LoggerPlugin("order-service"));
// Built-in metrics
const metrics = MetricsPlugin();
client.use(metrics);
// Later
console.log(metrics.snapshot());
// { sent: 100, succeeded: 97, failed: 3, retries: 5, successRate: 0.97, avgDurationMs: 212, ... }
metrics.reset();Custom Plugin
client.use({
name: "slack-alerts",
onError: async ({ to, error }) => {
await slackClient.send(
`SMS failed to ${to}: [${error.code}] ${error.message}`,
);
},
onRetry: async ({ to, attempt }) => {
if (attempt >= 2) console.warn(`High retry count for ${to}`);
},
});Hook Reference
| Hook | When it fires |
| ----------- | --------------------------------------------- |
| onSend | Before every send attempt |
| onSuccess | After a successful delivery |
| onError | After all retries are exhausted |
| onRetry | On each retry attempt |
| onDrop | When a queued job is discarded (TTL or dedup) |
Fault Tolerance
Retry Handler
Automatically retries transient failures (network errors, 5xx, rate limits). Never retries client-side errors (VALIDATION_ERROR, TEMPLATE_ERROR). Respects Retry-After headers on 429s.
Circuit Breaker
After N consecutive failures the circuit opens — all calls immediately fail with CircuitOpenError instead of hammering a down service. After a timeout the circuit goes half-open and probes before closing again.
console.log(client.circuitState); // "CLOSED" | "OPEN" | "HALF_OPEN"
client.resetCircuit(); // manual reset after fixing downstreamFull Configuration Reference
const client = new SMSClient({
baseUrl: "https://api.your-service.com",
apiKey: "your-access-token",
projectKey: "your-repo-token",
retry: {
maxAttempts: 4,
delay: 300,
strategy: "exponential", // "fixed" | "exponential"
jitter: true, // ±20% randomness
},
circuitBreaker: {
threshold: 5, // failures to open circuit
timeout: 30_000, // ms before half-open probe
successThreshold: 2, // probes to fully close
},
concurrency: 5,
batchSize: 50,
plugins: [LoggerPlugin("app")],
});