π°οΈ API Sentinel
A structured, developer-friendly networking and debugging layer for Flutter.
api_sentinel provides a clean and consistent way to handle API requests, error mapping, and runtime debugging through an on-screen overlay.
β¨ Features
β
Unified API call interface via ApiService.instance.request()
β
Customizable callbacks for success, Dio exceptions, and general exceptions
β
Centralized error handling and message parsing
β
Floating on-screen debug overlay to visualize requests
β
Generate curl commands for every request
β
Copy generated curl with a single tap
β
View request headers
β
View response headers
β
Built with dio and get (GetX) for lightweight reactivity
β
Supports Android, iOS, and Web
β
Built-in Unauthorized detection with callback (unauthorized status code, customizable unauthorized callback)
β
Optional network monitoring callback with structured error params
β
Secret knock gesture to reveal a hidden debug entry point
β
TOTP-gated access to the debug overlay in release builds
π¦ Installation
Add this line to your pubspec.yaml:
dependencies:
api_sentinel: ^2.0.0
Then run:
flutter pub get
π§ Architecture Overview
The library is built around three core layers:
| Layer | Description |
|---|---|
| ApiService | Handles all HTTP requests (GET, POST, PUT, PATCH, DELETE) through Dio. |
| ErrorHandler | Converts all errors (network, timeout, response, etc.) into readable Failure objects. |
| DebugOverlay | Shows all ongoing and past requests on top of your UI during runtime (toggleable). |
| SecretKncock and Totp Flow | Trigger via custom secret knock and accept Authenticator secret 6 digit codes. |
βοΈ Usage
1οΈβ£ Initialize the Service
ApiService.instance.init(
baseUrl: 'YOUR_BASE_URL',
needToShowLog: false,
needToLogRequests: false,
unauthorizedStatusCode: 401,
onUnauthorizedCallBack: () {
// Handle session expiration or redirect to login.
},
networkMonitoringFunction: (params) {
print('Request URL: ${params.requestUrl}');
print('Status Code: ${params.statusCode}');
print('API Error: ${params.apiErrorMessage}');
print('Runtime Error: ${params.runTimeErrorType}');
}
);
2οΈβ£ Monitor Network Failures
networkMonitoringFunction receives a NetworkMonitoringParams object whenever a request fails with either a DioException or another runtime exception.
class NetworkMonitoringParams {
final StackTrace? stackTrace;
final String? requestUrl;
final int? statusCode;
final String? apiErrorMessage;
final String? errorMessage;
final Object? runTimeErrorType;
}
Use it to capture the request URL, HTTP status code, parsed API error message, raw error text, and the original runtime error object in one place.
3οΈβ£ Make a Request
Each request is wrapped with ApiService.instance.request()
This ensures that error handling, logging, and overlay integration all happen automatically.
await ApiService.instance.request(
method: HttpMethod.get,
url: 'SOME_REQUEST',
onSuccess: (response) {
print('β
Success: ${response.data}');
},
onCatchDioException: (error) {
print('β Dio Error: ${handleErrorMessage(error)}');
},
onCatchException: (error) {
print('π₯ Exception: ${handleErrorMessage(error)}');
},
);
This pattern applies to any HTTP method β just change the method and url.
4οΈβ£ Supported HTTP Methods
You can use all standard HTTP verbs through the HttpMethod enum:
enum HttpMethod { get, post, put, patch, delete }
π§© Error Architecture
| Error Type | Source | Failure Example |
|---|---|---|
| Connection Timeout | Dio | (-1) Connection timed out |
| Bad Request | HTTP 400 | Bad Request |
| Unauthorized | HTTP 401 | Unauthorized access |
| Forbidden | HTTP 403 | Access denied |
| Not Found | HTTP 404 | Resource not found |
| Server Error | HTTP 500 | Internal server error |
| Cancelled | Dio CancelToken | Request cancelled |
| Unknown | Fallback | Something went wrong |
π Secret Knock & TOTP-Gated Debug Access (v2.0.0)
In release builds, the debug overlay should stay hidden until an authorized user proves access. API Sentinel provides a secret knock gesture detector, a TOTP verification flow, and an AccessController that unlocks the log overlay after a valid code.
Flow overview
User performs knock pattern on a widget
β
SecretKnockDetector fires onSecretKnock
β
You show TotoSecretSection (dialog, bottom sheet, etc.)
β
User enters 6-digit TOTP from authenticator app
β
AccessController.validateCode() succeeds
β
AccessController.enableDebugFeatures() β isDebugFeaturesAccessible = true
β
DebugOverlayWidget becomes visible
1οΈβ£ Register AccessController
Register the controller once near the root of your app (before any widget that needs it):
import 'package:api_sentinel/api_sentinel.dart';
import 'package:get/get.dart';
class _MyAppState extends State<MyApp> {
final AccessController accessController = Get.put(
AccessController(),
tag: AllControllerKeys.accessControllerKey,
);
// ...
}
| Symbol | Role |
|---|---|
AccessController |
Holds TOTP secret, validates codes, exposes isDebugFeaturesAccessible |
AllControllerKeys.accessControllerKey |
Stable GetX tag so TotoSecretSection can Get.find the same instance |
Key APIs
| Method / property | Description |
|---|---|
initialize() |
Loads or generates the TOTP secret. Call early (e.g. when the OTP UI opens). |
validateCode(String code) |
Returns true when the 6-digit code matches (Β±1 time window). |
enableDebugFeatures() |
Sets isDebugFeaturesAccessible to true. |
isDebugFeaturesAccessible |
Reactive RxBool β gate the overlay behind this. |
resetSecret() |
Clears the dev-only secure-storage secret (debug builds without dart-define). |
2οΈβ£ Wrap a widget with SecretKnockDetector
Pick one knock pattern for your app and use it consistently. The example uses SecretPatterns.accessible (double-tap, then long-press):
SecretKnockDetector(
knockPattern: SecretPatterns.accessible,
onSecretKnock: () {
showDialog(
context: context,
builder: (_) => Dialog(
child: const TotoSecretSection(),
),
);
},
child: const Text('App title'), // any widget β title, logo, version label, etc.
)
Available patterns (SecretPatterns):
| Pattern | Gesture |
|---|---|
accessible |
Double-tap β long-press |
tripleLongPress |
Long-press Γ 3 |
shaveAndHaircut |
Tap β tap β long-press |
morseSOS |
TapΓ3 β long-pressΓ3 β tapΓ3 |
You can also pass a custom List<KnockType> to knockPattern. Steps must be completed within resetTimeout (default 2 seconds between steps).
3οΈβ£ Show TotoSecretSection on knock
TotoSecretSection is a pre-built OTP field. It finds AccessController by tag, calls initialize() on mount, validates on submit, and calls enableDebugFeatures() on success.
Use it wherever you like β dialog, bottom sheet, or full-screen route:
onSecretKnock: () {
showModalBottomSheet(
context: context,
builder: (_) => const Padding(
padding: EdgeInsets.all(24),
child: TotoSecretSection(),
),
);
},
You can also build your own UI and call accessController.validateCode(code) directly.
4οΈβ£ Gate DebugOverlayWidget behind access
Only show the floating log button after TOTP succeeds:
Stack(
children: [
const MyHomePage(),
Obx(() {
if (!accessController.isDebugFeaturesAccessible.value) {
return const SizedBox.shrink();
}
return const DebugOverlayWidget();
}),
],
)
In debug builds without dart-defines, the first run logs an otpauth:// URI to the console β scan it with Google Authenticator. In release builds, the secret comes from dart-define (see below).
5οΈβ£ Pass the TOTP secret with --dart-define
For release (and local testing that matches release), split your Base32 secret into two parts so neither half is useful alone:
flutter build apk --release \
--dart-define=TOTP_SECRET_PART1="FIRST_HALF_OF_BASE32_SECRET" \
--dart-define=TOTP_SECRET_PART2="SECOND_HALF_OF_BASE32_SECRET"
The app concatenates both parts at compile time: TOTP_SECRET_PART1 + TOTP_SECRET_PART2.
Local run with embedded secret:
flutter run --release \
--dart-define=TOTP_SECRET_PART1="JBSWY3DPEHPK3PXP" \
--dart-define=TOTP_SECRET_PART2="GEZDGNBVGY3TQOJQ"
Important: Register the full combined secret in your authenticator app, not each part separately.
CI / Xcode / Gradle
Add the same defines to your build pipeline:
# GitHub Actions example
- run: flutter build ipa --release
--dart-define=TOTP_SECRET_PART1=${{ secrets.TOTP_PART1 }}
--dart-define=TOTP_SECRET_PART2=${{ secrets.TOTP_PART2 }}
| Build mode | Secret source |
|---|---|
| Release | TOTP_SECRET_PART1 + TOTP_SECRET_PART2 (required) |
| Debug + dart-define | Same embedded secret (for testing release behaviour) |
| Debug, no dart-define | Auto-generated secret stored in secure storage; URI printed to console |
Full integration example
See example/lib/main.dart for a working setup with SecretKnockDetector, TotoSecretSection, AccessController, and DebugOverlayWidget.
π§° Debug Overlay
π§ Floating Draggable Widget
A persistent, draggable button gives quick access to real-time logs. In production, gate it behind AccessController.isDebugFeaturesAccessible (see Secret Knock & TOTP).
Stack(
children: [
const MyHomePage(),
Obx(() {
if (!accessController.isDebugFeaturesAccessible.value) {
return const SizedBox.shrink();
}
return const DebugOverlayWidget();
}),
],
)
Inside, youβll see:
- A real-time log list for each request
- Search and filter by method/status code
- Tap any log to expand request/response JSON
- Toggle between Tree View and Pretty JSON
- Click to expand full-screen
- View request headers
- View response headers
- Generate a curl command from the request
- Copy the generated curl to the clipboard
π³ JSON Tree Viewer
Displays structured hierarchical JSON for nested inspection.
π¨ Pretty JSON Viewer
Shows syntax-colored formatted JSON text.
π₯ Full-Screen View
Click the expand icon (π) in the corner to open the full JSON view for better readability.
Whenever your app performs an API call through ApiService, it will appear in a floating overlay with:
- Method type (GET/POST/PUT/PATCH/DELETE)
- Status code
- Response time
- Response preview
- Request headers
- Response headers
- Generated curl command with one-tap copy
π§ͺ Example Project
A complete example app is included under the example/ directory.
Debug mode (auto-generated TOTP secret printed to console):
cd example
flutter run
With embedded TOTP secret (same as release):
cd example
flutter run \
--dart-define=TOTP_SECRET_PART1="YOUR_PART_1" \
--dart-define=TOTP_SECRET_PART2="YOUR_PART_2"
The example demonstrates:
- API calls via
ApiService(GET, POST, PUT, PATCH, DELETE) SecretKnockDetectorwithSecretPatterns.accessibleTotoSecretSectionshown in a dialog after the knockDebugOverlayWidgetgated behindAccessController.isDebugFeaturesAccessible
π License
MIT License Β© 2025 Developed and maintained by Aref Yazdkhasti
π¬ Contribution
Contributions are welcome!
If youβd like to improve the debugging UI, extend the ErrorHandler, or support additional APIs, open a PR or issue.
π§ Future Plans
Response caching layerRetry strategy for failed requests and 401 unauthenticated requestFilterable API session logs
API Sentinel β Because understanding your API should be as clear as your code.
Libraries
- api_sentinel
- controllers/access_controller
- controllers/api_service
- controllers/debug_overlay/curl_builder
- controllers/debug_overlay/curl_capturing_adapter
- controllers/debug_overlay/debug_interceptor
- controllers/debug_overlay/debug_log_controller
- controllers/error_handler/error_handler
- global_configs
- models/debug_log_entry
- models/failure
- models/monitoring/network_monitoring_function
- models/monitoring/network_monitoring_params
- models/webService/data_source
- models/webService/response_code
- models/webService/response_message
- res/all_controller_keys
- res/release_config
- screens/debug_page
- screens/json_full_screen_view
- widgets/debug_overlay_widget
- widgets/json_tree_view
- widgets/pretty_json_view
- widgets/search_input_field
- widgets/secret_knock_detector
- widgets/totp_secret_section