Skip to content

Commit f813cb7

Browse files
committed
feat(remote_config): add Firebase Remote Config support
* feat: add Firebase Remote Config support Implements Firebase Remote Config for the Dart Admin SDK with full template management plus server-side template evaluation.
1 parent b3ea208 commit f813cb7

22 files changed

Lines changed: 4527 additions & 1 deletion

.github/workflows/build.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,15 @@ jobs:
287287
- name: Run WIF auth test
288288
run: dart test test/integration/app/firebase_app_prod_test.dart --concurrency=1 --tags wif
289289

290+
- name: Run Remote Config integration tests
291+
env:
292+
SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }}
293+
run: |
294+
project_id="${SERVICE_ACCOUNT##*@}"
295+
project_id="${project_id%%.iam.gserviceaccount.com}"
296+
RC_TEST_PROJECT_ID="$project_id" \
297+
dart test test/integration/remote_config/ --concurrency=1
298+
290299
publish:
291300
name: Publish verification
292301
runs-on: ubuntu-latest

packages/firebase_admin_sdk/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## 0.5.2-wip
22

3+
- Add Remote Config support: template management and server-side template evaluation.
34
- Remove dependency on `package:equatable`.
45
- Make `Query`, `CollectionReference`, `DocumentReference`, and `CollectionGroup` mockable.
56
- `Credential.createClient(List<String> scopes)` — create an authenticated `AuthClient` directly

packages/firebase_admin_sdk/README.md

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
- [App Check](#app-check)
1919
- [Firestore](#firestore)
2020
- [Functions](#functions)
21+
- [Remote Config](#remote-config)
2122
- [Messaging](#messaging)
2223
- [Storage](#storage)
2324
- [Security Rules](#security-rules)
@@ -57,7 +58,7 @@ The Firebase Admin Dart SDK currently supports the following Firebase services:
5758
| Machine Learning | 🔴 | |
5859
| Messaging | 🟢 | |
5960
| Project Management | 🔴 | |
60-
| Remote Config | 🔴 | |
61+
| Remote Config | 🟢 | |
6162
| Security Rules | 🟢 | |
6263
| Storage | 🟢 | Via [package:google_cloud_storage] |
6364

@@ -583,6 +584,84 @@ await queue.enqueue(
583584
await queue.delete('payment-order-456');
584585
```
585586

587+
### Remote Config
588+
589+
```dart
590+
import 'package:firebase_admin_sdk/firebase_admin_sdk.dart';
591+
import 'package:firebase_admin_sdk/remote_config.dart';
592+
593+
final app = FirebaseApp.initializeApp();
594+
final remoteConfig = app.remoteConfig();
595+
```
596+
597+
#### getTemplate / publishTemplate
598+
599+
```dart
600+
// Read the active template, edit one parameter, and publish.
601+
final template = await remoteConfig.getTemplate();
602+
final updated = RemoteConfigTemplate(
603+
etag: template.etag,
604+
conditions: template.conditions,
605+
parameters: {
606+
...template.parameters,
607+
'welcome_message': RemoteConfigParameter(
608+
defaultValue: const ExplicitParameterValue(value: 'Hello'),
609+
description: 'Updated greeting',
610+
),
611+
},
612+
parameterGroups: template.parameterGroups,
613+
version: template.version,
614+
);
615+
final published = await remoteConfig.publishTemplate(updated);
616+
print('Published version: ${published.version?.versionNumber}');
617+
```
618+
619+
#### validateTemplate
620+
621+
```dart
622+
// Server-side validation without publishing.
623+
final validated = await remoteConfig.validateTemplate(template);
624+
print('Template OK; etag: ${validated.etag}');
625+
```
626+
627+
#### listVersions / rollback
628+
629+
```dart
630+
final versions = await remoteConfig.listVersions(
631+
ListVersionsOptions(pageSize: 10),
632+
);
633+
for (final v in versions.versions) {
634+
print('v${v.versionNumber}: ${v.description}');
635+
}
636+
637+
// Roll back to a previous version.
638+
await remoteConfig.rollback('5');
639+
```
640+
641+
#### Server-side evaluation
642+
643+
Use [`getServerTemplate`](https://firebase.google.com/docs/remote-config/server-side-config) to fetch a template that the SDK can evaluate locally — useful for runtime feature flags, A/B test bucketing, and server-rendered configuration.
644+
645+
```dart
646+
final template = await remoteConfig.getServerTemplate(
647+
defaultConfig: {'enable_new_ui': false, 'max_items': 50},
648+
);
649+
650+
// Evaluate against an EvaluationContext: randomizationId is used by percent
651+
// conditions, custom signals are used by string/numeric/semver conditions.
652+
final config = template.evaluate(
653+
EvaluationContext(
654+
randomizationId: '<user-id>',
655+
customSignals: {'app_version': '2.3.1', 'country': 'US'},
656+
),
657+
);
658+
659+
if (config.getBoolean('enable_new_ui')) {
660+
// ...
661+
}
662+
print('max items: ${config.getInt('max_items')}');
663+
```
664+
586665
### Messaging
587666

588667
```dart

packages/firebase_admin_sdk/example/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,6 @@ Some examples require a real project and credentials and are commented out by de
4646
- **App Check**
4747
- **Messaging**
4848
- **Security Rules**
49+
- **Remote Config**
4950

5051
You can uncomment them in `bin/example.dart` to try them out if you have a properly configured project.

packages/firebase_admin_sdk/example/bin/example.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import 'package:firebase_admin_sdk_example/auth_example.dart';
2121
import 'package:firebase_admin_sdk_example/firestore_example.dart';
2222
import 'package:firebase_admin_sdk_example/functions_example.dart';
2323
import 'package:firebase_admin_sdk_example/messaging_example.dart';
24+
import 'package:firebase_admin_sdk_example/remote_config_example.dart';
2425
import 'package:firebase_admin_sdk_example/security_rules_example.dart';
2526
import 'package:firebase_admin_sdk_example/storage_example.dart';
2627

@@ -55,6 +56,9 @@ Future<void> main() async {
5556

5657
// Uncomment to run security rules example (requires a real project and credentials)
5758
// await securityRulesExample(admin);
59+
60+
// Uncomment to run remote config example (requires a real project and credentials)
61+
// await remoteConfigExample(admin);
5862
} finally {
5963
await admin.close();
6064
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import 'package:firebase_admin_sdk/firebase_admin_sdk.dart';
16+
import 'package:firebase_admin_sdk/remote_config.dart';
17+
18+
Future<void> remoteConfigExample(FirebaseApp admin) async {
19+
print('\n### Remote Config Example ###\n');
20+
21+
final remoteConfig = admin.remoteConfig();
22+
23+
// Example 1: Read the active template.
24+
RemoteConfigTemplate? template;
25+
try {
26+
print('> Fetching active template...\n');
27+
template = await remoteConfig.getTemplate();
28+
print('Template fetched!');
29+
print(' - ETag: ${template.etag}');
30+
print(' - Conditions: ${template.conditions.length}');
31+
print(' - Parameters: ${template.parameters.length}');
32+
print(' - Parameter groups: ${template.parameterGroups.length}');
33+
if (template.version != null) {
34+
print(' - Version: ${template.version!.versionNumber}');
35+
}
36+
print('');
37+
} on FirebaseRemoteConfigException catch (e) {
38+
print('> Remote Config error: ${e.code} - ${e.message}');
39+
return;
40+
} catch (e) {
41+
print('> Error fetching template: $e');
42+
return;
43+
}
44+
45+
// Example 2: Validate a modified template without publishing.
46+
try {
47+
print('> Validating a modified template (no publish)...\n');
48+
final modified = RemoteConfigTemplate(
49+
etag: template.etag,
50+
conditions: template.conditions,
51+
parameters: <String, RemoteConfigParameter>{
52+
...template.parameters,
53+
'dart_admin_sdk_demo': RemoteConfigParameter(
54+
defaultValue: const ExplicitParameterValue(value: 'hello'),
55+
description: 'Demo parameter from the Dart Admin SDK example.',
56+
valueType: ParameterValueType.string,
57+
),
58+
},
59+
parameterGroups: template.parameterGroups,
60+
version: template.version,
61+
);
62+
final validated = await remoteConfig.validateTemplate(modified);
63+
print('Template validated!');
64+
print(' - ETag (restored): ${validated.etag}');
65+
print('');
66+
} on FirebaseRemoteConfigException catch (e) {
67+
print('> Remote Config error: ${e.code} - ${e.message}');
68+
} catch (e) {
69+
print('> Error validating template: $e');
70+
}
71+
72+
// Example 3: List published versions.
73+
try {
74+
print('> Listing published versions (page size 5)...\n');
75+
final result = await remoteConfig.listVersions(
76+
ListVersionsOptions(pageSize: 5),
77+
);
78+
print('Got ${result.versions.length} version(s):');
79+
for (final v in result.versions) {
80+
print(' - v${v.versionNumber}: ${v.description ?? '(no description)'}');
81+
}
82+
print('');
83+
} on FirebaseRemoteConfigException catch (e) {
84+
print('> Remote Config error: ${e.code} - ${e.message}');
85+
} catch (e) {
86+
print('> Error listing versions: $e');
87+
}
88+
89+
// Example 4: Server-side template evaluation.
90+
try {
91+
print('> Fetching server template and evaluating...\n');
92+
final serverTemplate = await remoteConfig.getServerTemplate(
93+
defaultConfig: const <String, Object>{
94+
'enable_new_ui': false,
95+
'max_items': 50,
96+
},
97+
);
98+
final config = serverTemplate.evaluate(
99+
const EvaluationContext(
100+
randomizationId: 'demo-user-id',
101+
customSignals: <String, Object>{
102+
'app_version': '2.3.1',
103+
'country': 'US',
104+
},
105+
),
106+
);
107+
print('Server config evaluated!');
108+
print(' - enable_new_ui: ${config.getBoolean('enable_new_ui')}');
109+
print(' - max_items: ${config.getInt('max_items')}');
110+
final all = config.getAll();
111+
print(' - total resolved keys: ${all.length}');
112+
print('');
113+
} on FirebaseRemoteConfigException catch (e) {
114+
print('> Remote Config error: ${e.code} - ${e.message}');
115+
} catch (e) {
116+
print('> Error evaluating server template: $e');
117+
}
118+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
export 'src/remote_config/remote_config.dart'
16+
hide
17+
ConditionEvaluator,
18+
RemoteConfigHttpClient,
19+
RemoteConfigHttpResult,
20+
RemoteConfigRequestHandler,
21+
remoteConfigErrorCodeMapping;

packages/firebase_admin_sdk/lib/src/app.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import '../auth.dart';
3232
import '../firestore.dart';
3333
import '../functions.dart';
3434
import '../messaging.dart';
35+
import '../remote_config.dart';
3536
import '../security_rules.dart';
3637
import '../storage.dart';
3738
import 'utils/utils.dart';

packages/firebase_admin_sdk/lib/src/app/firebase_app.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,11 @@ class FirebaseApp {
229229
/// Returns a cached instance if one exists, otherwise creates a new one.
230230
Messaging messaging() => Messaging.internal(this);
231231

232+
/// Gets the Remote Config service instance for this app.
233+
///
234+
/// Returns a cached instance if one exists, otherwise creates a new one.
235+
RemoteConfig remoteConfig() => RemoteConfig.internal(this);
236+
232237
/// Gets the Security Rules service instance for this app.
233238
///
234239
/// Returns a cached instance if one exists, otherwise creates a new one.

packages/firebase_admin_sdk/lib/src/app/firebase_service.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ enum FirebaseServiceType {
2020
auth(name: 'auth'),
2121
firestore(name: 'firestore'),
2222
messaging(name: 'messaging'),
23+
remoteConfig(name: 'remote-config'),
2324
securityRules(name: 'security-rules'),
2425
functions(name: 'functions'),
2526
storage(name: 'storage');

0 commit comments

Comments
 (0)