Flextuma is a configurable, multi-tenant messaging gateway built on Spring Boot. It serves multiple organisations from a single deployment with full data isolation, and supports SMS delivery today with WhatsApp and Email on the roadmap.
| Requirement | Version |
|---|---|
| Java | 17+ |
| Docker & Docker Compose | Any recent version |
| Gradle | Provided via wrapper (./gradlew) |
The application requires PostgreSQL and Redis to be available before startup. These are not provisioned by the included compose.yaml — they must be provided externally.
git clone <repository-url>
cd flextumaCreate a .env file in the root directory or export the variables in your shell:
| Variable | Required | Default | Description |
|---|---|---|---|
SPRING_DATASOURCE_URL |
✅ | — | JDBC URL, e.g. jdbc:postgresql://host:5432/db |
SPRING_DATASOURCE_USERNAME |
✅ | — | Database username |
SPRING_DATASOURCE_PASSWORD |
✅ | — | Database password |
SPRING_DATA_REDIS_HOST |
✅ | — | Redis hostname |
SPRING_DATA_REDIS_PORT |
❌ | 6379 |
Redis port |
HIKARI_MAX_POOL |
❌ | 10 |
Max JDBC connection pool size |
SMS_PRICE_PER_SEGMENT |
❌ | 20.0 |
Price per SMS segment (in TZS) |
./gradlew clean build -x testdocker compose up --buildThe application starts on http://localhost:8080.
./gradlew bootRun./gradlew build -tFlextuma follows a layered architecture with a shared core library and feature-based modules.
src/main/java/com/flexcodelabs/flextuma/
├── core/
│ ├── config/ # App startup, Jackson, request logging, cookie auth config
│ ├── context/ # TenantContext (ThreadLocal — reserved, not yet active)
│ ├── annotations/ # @FeatureGate — method-level feature flag annotation
│ ├── aspects/ # FeatureGateAspect — AOP enforcement of @FeatureGate
│ ├── controllers/ # BaseController<T, S> — generic CRUD for all modules
│ ├── dtos/ # Pagination<T> response wrapper
│ ├── entities/
│ │ ├── base/ # BaseEntity, NameEntity, Owner (MappedSuperclasses)
│ │ ├── auth/ # User, Role, Privilege, Organisation
│ │ ├── connector/ # ConnectorConfig
│ │ ├── contact/ # Contact
│ │ ├── feature/ # TenantFeature — per-org feature flags
│ │ ├── metadata/ # Tag, ListEntity
│ │ └── sms/ # SmsConnector, SmsTemplate, SmsLog
│ ├── enums/ # AuthType, CategoryEnum, UserType, FilterOperator
│ ├── exceptions/ # Global exception handling
│ ├── helpers/ # Specification builder, filters, masking, template utils
│ ├── interceptors/ # Entity audit interceptor
│ ├── repositories/ # BaseRepository + all JPA repositories
│ ├── security/ # SecurityConfig, SecurityUtils, CustomSecurityExceptionHandler
│ ├── senders/ # SmsSender interface + BeemSender, NextSmsSender
│ └── services/ # BaseService<T>, SmsSenderRegistry, DataSeederService
└── modules/
├── auth/ # User, Role, Privilege, Organisation controllers & services
├── connector/ # ConnectorConfig + DataHydratorService
├── contact/ # Contact management
├── feature/ # TenantFeature — per-org feature flag management
├── metadata/ # Tags and Lists
├── notification/ # Notification management
└── sms/ # SmsConnector, SmsTemplate controllers & services
POST /api/register
Content-Type: application/json
{
"username": "string",
"email": "string",
"phoneNumber": "string",
"password": "string",
"organisation": { "id": "uuid" }
}- Rate Limited: Prevents brute force registration attempts
- Verification: Automatically sends verification codes to email and phone
- Bonus Credits: Awards configurable SMS segments on successful registration
POST /api/login
Content-Type: application/json
{
"username": "string",
"password": "string"
}- Rate Limited: Blocks excessive login attempts
- Session Management: Creates HttpOnly session cookie backed by Redis
- Security Logging: Records all login attempts for audit
POST /api/logout- Clears session cookie and logs security event
GET /api/me- Returns authenticated user's profile information
POST /api/verify
Content-Type: application/json
{
"identifier": "email@domain.com",
"code": "123456"
}POST /api/resendVerification
Content-Type: application/json
{
"identifier": "email@domain.com"
}POST /api/changePassword
Content-Type: application/json
{
"currentPassword": "string",
"newPassword": "string",
"confirmPassword": "string"
}# Standard CRUD operations
GET|POST|PUT|DELETE /api/personalAccessTokens- Enables API authentication without session cookies
- Ideal for integrations and automated systems
Full CRUD operations for SMS campaigns with scheduling capabilities:
{
"name": "Campaign Name",
"description": "Campaign description",
"template": { "id": "template-uuid" },
"scheduledAt": "2024-01-15T10:30:00",
"status": "DRAFT|SCHEDULED|RUNNING|COMPLETED",
"recipients": "phone1,phone2,phone3",
"connector": { "id": "connector-uuid" }
}- DRAFT - Initial state, can be modified
- SCHEDULED - Set for future delivery
- RUNNING - Currently being processed
- COMPLETED - Finished processing
{
"balance": 1000.0000,
"smsCost": 20.00,
"currency": "TZS",
"type": "SMS",
"value": 20000.00
}- Multi-currency Support - Configurable currency per wallet
- Real-time Balance - Updated immediately after SMS sending
- Cost Tracking - Per-segment cost calculation
- Transaction History - Complete audit trail via WalletTransaction
- Registration bonus credits configurable via environment
- Pre-flight balance checks before SMS sending
- Automatic deduction upon successful delivery
POST /api/smsTemplates/preview
Content-Type: application/json
{
"template": "Hello {{name}}, your order {{orderId}} is ready!",
"variables": {
"name": "John Doe",
"orderId": "12345"
}
}Response:
{
"rendered": "Hello John Doe, your order 12345 is ready!",
"segments": 1,
"encoding": "GSM-7",
"charactersRemaining": 145,
"cost": 20.00,
"pricePerSegment": 20.00
}POST /api/smsLogs/{id}/retry- Retries failed SMS messages with original parameters
- Updates log status and provider response
GET /api/systemLogs?level=ERROR&source=SMS&from=2024-01-01T00:00:00Parameters:
level- Log level (ERROR, WARN, INFO, DEBUG)source- Component source (SMS, AUTH, WEBHOOK, etc.)traceId- Request trace identifierfrom/to- Date range filtering
GET /api/systemLogs/tail?level=ERROR
Accept: text/event-stream- Server-Sent Events (SSE) for live log monitoring
- Filterable by log level
GET /api/systemLogs/healthReturns system health metrics including database status, memory usage, and active connections.
DELETE /api/systemLogs/purge?days=30- Purges log entries older than specified days
- Returns count of deleted records
POST /api/webhooks/{provider}
Content-Type: application/json
{
"messageId": "provider-message-id",
"status": "DELIVERED|FAILED|PENDING",
"timestamp": "2024-01-15T10:30:00Z"
}Supported Providers:
beem- Beem SMS providernext- NextSMS provider
POST /api/webhooks/{connectorId}/sms
Content-Type: application/json
{
"provider": "beem",
"templateCode": "WELCOME_MSG",
"content": "Custom message content",
"filterQuery": "status=active"
}Features:
- Fetches recipients from external ERP systems
- Supports both template-based and raw content
- Automatic queueing for async processing
POST /api/notifications
Content-Type: application/json
{
"to": "+255123456789",
"templateCode": "WELCOME_MSG",
"variables": {
"name": "John Doe",
"company": "ACME Corp"
}
}POST /api/notifications/raw
Content-Type: application/json
{
"to": "+255123456789",
"content": "Direct message content",
"provider": "beem"
}POST /api/apps
Content-Type: multipart/form-data
appName: myapp
version: 1.0.0
file: [application.zip]- SUPER_ADMIN only endpoint
- Uploads and extracts application packages
- Supports system extensions and plugins
All entities extend one of:
| Class | Adds |
|---|---|
BaseEntity |
id (UUID), created, updated, active, code |
NameEntity extends BaseEntity |
name, description |
Owner extends BaseEntity |
createdBy (User), updatedBy (User) with @CreatedBy audit |
Every resource gets full CRUD for free by extending these:
| HTTP Method | Endpoint | Action |
|---|---|---|
GET |
/api/{resource} |
Paginated list with optional filter and fields params |
GET |
/api/{resource}/{id} |
Get by ID |
POST |
/api/{resource} |
Create |
PUT |
/api/{resource}/{id} |
Update (null-safe partial update) |
DELETE |
/api/{resource}/{id} |
Delete (with optional pre-delete validation) |
Filter syntax: ?filter=field:OPERATOR:value — supports EQ, NE, LIKE, ILIKE, IN, GT, LT.
Every resource defines permission constants (READ_*, ADD_*, UPDATE_*, DELETE_*). BaseService checks these against the current user's granted authorities before every operation. Users with SUPER_ADMIN or ALL bypass all checks.
Flextuma supports per-organisation feature flags via the @FeatureGate AOP annotation. This lets you gate specific capabilities per tenant without a code deploy — useful for subscription tiers, beta rollouts, or temporarily suspending access.
- Annotate any service method with
@FeatureGate("FEATURE_KEY") - Spring AOP intercepts the call and checks the
tenantfeaturetable for the calling user's organisation - If a record with
enabled = falseexists →403 Forbiddenis thrown before the method runs - If no record exists → the feature is allowed (default-open: you only need records for restrictions)
- Users with no organisation (SUPER_ADMIN, system users) always bypass the check
Step 1. Pick a SCREAMING_SNAKE_CASE key and annotate the service method:
// modules/notification/services/NotificationService.java
@Async
@FeatureGate("BULK_CAMPAIGN")
public void sendCampaign(Campaign campaign, String username) {
// 403 thrown here automatically if org has BULK_CAMPAIGN disabled
}Step 2. Add it to the feature keys table in this README (see below).
That's it. No DB schema changes, no config files.
Feature flags and permissions work together but guard different things:
| Layer | Enforced by | Question answered |
|---|---|---|
| Permission | BaseService.checkPermission() |
Does this user's role allow this action? |
| Feature flag | @FeatureGate AOP |
Does this organisation's plan include this capability? |
@FeatureGate("BULK_CAMPAIGN") // ← org-level: is this feature enabled for the tenant?
public void sendCampaign(...) {
checkPermission("SEND_BULK"); // ← user-level: does the user have the right role?
...
}| Scenario | Result |
|---|---|
User lacks SEND_BULK role |
checkPermission() throws 403 |
| User has role, but org is restricted | @FeatureGate throws 403 |
| User has role AND org has feature | ✅ Proceeds |
### Create a restriction (disable a feature for an org)
POST /api/tenantFeatures
Content-Type: application/json
{
"organisation": { "id": "<org-uuid>" },
"featureKey": "WHATSAPP_SEND",
"enabled": false
}
### Re-enable (e.g. after plan upgrade)
PUT /api/tenantFeatures/<feature-uuid>
Content-Type: application/json
{ "enabled": true }
### List all flags for inspection
GET /api/tenantFeatures?filter=organisation:EQ:<org-uuid>Document every key here when you introduce it:
| Key | Controls | Default |
|---|---|---|
BULK_CAMPAIGN |
Bulk messaging to contact lists/tags | Open |
WHATSAPP_SEND |
WhatsApp channel sending | Open |
EMAIL_SEND |
Email channel sending | Open |
CONNECTOR_PULL |
Fetching contacts via external connector | Open |
Convention: All features are open by default. Only create
TenantFeaturerecords when you need to restrict an org. This keeps the table minimal and the logic simple.
| Client Type | Method | Usage |
|---|---|---|
| Browser/SPA | Session-based auth via POST /api/login |
Receives HttpOnly SESSION cookie (Redis-backed) |
| API/Testing | HTTP Basic Auth (Authorization: Basic base64(user:pass)) |
Also creates session for convenience |
| Integrations | Personal Access Token (PAT) | Token-based auth for automated systems |
Authentication Endpoints:
- Registration: Blocks after excessive attempts with configurable timeout
- Login: Prevents brute force attacks with progressive delays
- Verification: Limits resend attempts to prevent abuse
Rate Limit Response:
{
"error": "Rate limit exceeded",
"message": "Too many attempts. Try again in 300 seconds.",
"retryAfter": 300
}- Token Method: Cookie-based
XSRF-TOKEN+ headerX-CSRF-TOKEN - Exemptions:
/api/login,/api/webhooks/**(for provider callbacks) - Browser Support: Automatic for modern SPAs using
withCredentials: true
- Storage: Redis-based session persistence
- Cookie:
SESSION, HttpOnly,SameSite=Lax - Concurrency: Maximum 1 concurrent session per user
- Timeout: Configurable session expiration
All security events are automatically logged:
- Login attempts (success/failure)
- Registration attempts
- Password changes
- Logout events
- Verification attempts
Log Format:
{
"timestamp": "2024-01-15T10:30:00Z",
"level": "INFO",
"source": "AUTH",
"event": "LOGIN_SUCCESS",
"username": "john.doe",
"ipAddress": "192.168.1.100",
"userAgent": "Mozilla/5.0...",
"traceId": "abc123-def456"
}Tenant-Aware Filtering:
- SUPER_ADMIN/ALL: Sees all records (no restriction)
- Organisation Users: Sees own records + org-wide data
- Unaffiliated Users: Sees only personal records
- System Entities: No filtering applied (e.g., Organisation)
Implementation: Automatic TenantAwareSpecification in BaseService
Manages users, roles, privilege-based RBAC, organisation membership, and API tokens.
User— linked to anOrganisation(one-to-many: many users per org).UserTypeenum (e.g.SYSTEM) identifies platform-level admins.Organisation— the multi-tenancy anchor. Each SACCO is one Organisation. All users of that SACCO share the sameorganisationId.Role→Privilege— fine-grained permission strings enforced inBaseService.PersonalAccessToken— API tokens for integrations and automated systems.
Additional Endpoints:
/api/register- User registration with verification/api/login- Authentication with rate limiting/api/logout- Session termination/api/me- Current user profile/api/verify- Email/phone verification/api/changePassword- Password management
Manages organizational wallets and SMS billing.
Wallet— per-organisation SMS credit balance with real-time updatesWalletTransaction— complete audit trail of all credit movements
Features:
- Multi-currency support (TZS, USD, etc.)
- Per-segment cost calculation
- Automatic credit deduction on SMS delivery
- Registration bonus credit allocation
- Pre-flight balance checks
Comprehensive SMS management with campaigns, templates, and delivery tracking.
SmsConnector— provider configuration (URL, API key/secret, sender ID, extra settings). One connector can be marked active at a time.SmsTemplate— message templates with{placeholder}variables, categorised byCategoryEnum(PROMOTIONAL, etc.). System templates are protected from deletion.SmsLog— records every sent message: recipient, content, status, provider response, error, and linked template.SmsCampaign— scheduled bulk messaging with status tracking (DRAFT, SCHEDULED, RUNNING, COMPLETED).SmsSendResult— standardized result object containing success/failure status, message ID, error codes, and full provider response data.SmsSenderRegistry— selects the activeSmsConnectorfrom the DB, finds the matchingSmsSenderimplementation by provider name, and dispatches the message.
Advanced Features:
- Template preview with cost calculation (
/api/smsTemplates/preview) - Failed message retry (
/api/smsLogs/{id}/retry) - Character encoding detection (GSM-7 vs UCS-2)
- Segment-based billing
Real-time notification dispatch and queue management.
- Template-based SMS - Send templated messages with variable substitution
- Raw SMS Sending - Direct content delivery without templates
- Queue Management - Async processing with status tracking
System monitoring, logging, and application management.
SystemLog— structured logging with filtering, search, and real-time streaming- App Management — application upload and plugin system
Features:
- Real-time log streaming via Server-Sent Events
- Log level filtering (ERROR, WARN, INFO, DEBUG)
- System health monitoring
- Log purging by date range
- Application package upload (SUPER_ADMIN only)
External integrations and delivery report handling.
- Delivery Report (DLR) Receiver — Accepts status updates from SMS providers
- Recipient Resolver Trigger — External ERP integration for bulk messaging
Supported Providers:
beem- Beem SMS provider DLRsnext- NextSMS provider DLRs
Configures how Flextuma connects to each organisation's external ERP/data source.
ConnectorConfig— stores the base URL, endpoint,AuthType(NONE,BASIC,BEARER,API_KEY), credentials (masked in responses), and a JSONPath mapping list (List<FieldMapping>) stored as JSONB.DataHydratorService— given atenantIdand amemberId, fetches the external ERP, applies the JSONPath mappings, and returns aMap<String, String>of system keys to values. Used to populate SMS template placeholders.
Contact and recipient management for messaging campaigns.
Per-organisation feature flag management for subscription tiers and access control.
Tag and list management for organizing contacts and content.
Two concrete SmsSender implementations:
| Provider | Class | Auth Method | Status |
|---|---|---|---|
| Beem | BeemSender |
API key + secret (Basic Auth header) | ✅ Production ready |
| NextSMS | NextSmsSender |
API key + secret (Basic Auth header) | ✅ Production ready |
Adding a new provider: implement SmsSender, annotate with @Service, and set the matching provider string on the SmsConnector record.
All SMS providers now return standardized SmsSendResult objects that include:
- Success/Failure Status - Boolean success flag with descriptive messages
- Message ID - Provider-specific message identifier for tracking
- Error Codes - Standardized error codes for failure scenarios
- Full Provider Response - Complete response data as
Map<String, Object>for debugging and audit
Response Processing Flow:
Provider HTTP Response → SmsSender.processResponse() → SmsSendResult → SmsLog.providerResponse
Key Features:
- Type-safe response mapping using Jackson
Map<String, Object>conversion - Automatic error extraction from provider error responses
- Detailed logging of provider responses for audit trails
- Consistent error handling across all SMS providers
Request with memberId
→ ConnectorConfigRepository.findByTenantId(tenantId)
→ Build URL: config.url + config.endpoint.replace("{id}", memberId)
→ Apply auth headers (BEARER / API_KEY / BASIC / NONE)
→ Parse JSON response with Jayway JsonPath
→ Map to internal keys via FieldMapping list
→ Return Map<String, String> for template rendering
| Client | Method |
|---|---|
| Browser / SPA | Session-based: POST credentials to /api/login → receive HttpOnly SESSION cookie (backed by Redis) |
| API/testing | HTTP Basic Auth (Authorization: Basic base64(user:pass)) — also accepted for session creation |
| Webhooks / PAT | Personal Access Token (planned) |
CSRF protection uses CookieCsrfTokenRepository (token sent as XSRF-TOKEN cookie, readable by SPA). Exemptions:
/api/login— no session exists yet at this point/api/webhooks/**— reserved for PAT-authenticated provider callbacks
Every paginated and list query automatically applies TenantAwareSpecification:
| User | Sees |
|---|---|
SUPER_ADMIN or ALL authority |
All records (no restriction) |
| User with an Organisation | Records they created or records created by any member of the same organisation |
| User with no Organisation | Only their own records |
Entities without createdBy (e.g. Organisation) |
No restriction applied |
This is enforced in BaseService.buildTenantSpec() — all subclass services benefit automatically.
- Sessions are stored in Redis (
@EnableRedisHttpSession) - Session cookie:
SESSION, HttpOnly,SameSite=Lax - Maximum 1 concurrent session per user
On startup, DataInitializer runs DataSeederService.seedSystemData(), which executes seed.sql via JDBC to ensure system-level data (privileges, default roles, system user) is present before the application accepts requests.
./gradlew testHTTP request files are in the /http directory. Use IntelliJ's HTTP client or any compatible tool. The login endpoint does not require a CSRF token. All subsequent mutating requests (POST/PUT/DELETE) must include the X-CSRF-TOKEN header (value from the XSRF-TOKEN response cookie).
### Login
POST http://localhost:8080/api/login
Content-Type: application/json
{"username": "admin", "password": "pass"}- Create an entity in
core/entities/extendingBaseEntity,NameEntity, orOwner - Define permission constants (
READ_*,ADD_*, etc.) on the entity - Create a
JpaRepositoryincore/repositories/ - Create a
Service extends BaseService<YourEntity>inmodules/.../services/ - Create a
Controller extends BaseController<YourEntity, YourService>inmodules/.../controllers/
See ROADMAP/roadmap.md for the full development roadmap, ROADMAP/architecture.md for the multi-channel notification architecture, and ROADMAP/roadmap-audit.md for the current implementation status of each item.
Recently completed:
- Admin Monitoring API enhancements (query by status, retry endpoint)
- Scheduling Engine (future-dated campaigns)
- Personal Access Token (PAT) entity and filter for API / gateway access
- Per-organisation feature flagging via
@FeatureGateAOP annotation -
TenantAwareSpecification— automatic org-scoped data isolation -
DataHydratorService— external ERP integration with JSONPath field mapping - Template placeholder engine (
{{variable}}syntax with missing-variable detection) - SMS segment calculator (GSM-7 vs Unicode encoding)
- Wallet & ledger system with pre-flight balance checks
- Async SMS dispatch worker (
@Scheduled+SmsLogstatus lifecycle) - Rate Limiter (Bucket4j per-tenant quotas)
- Webhook DLR receiver & Recipient Resolver Trigger API (
/api/webhooks...) - Character Count & Preview API (
/api/smsTemplates/previewreturning segment counts andcharactersRemainingbudget) - Real HTTP implementation for
NextSmsSenderwith provider response logging - Standardized
SmsSendResultservice with type-safe response handling
Immediate next steps:
- Database Partitioning for
sms_logtable - Multi-channel support (WhatsApp/Email)
The new WalletService handles crediting and debiting of accounts per organisation.
Currently, wallets must be topped up programmatically until an admin UI is built.
Example of topping up an account with 100,000 TZS dynamically inside a Service:
@Autowired
private WalletService walletService;
public void processManualTopup(User orgAdmin) {
BigDecimal amount = BigDecimal.valueOf(100000.00);
walletService.credit(orgAdmin, amount, "Manual Top Up", "REF-12345");
}