fetchApiKeys method
Fetches the project's API keys via the Management API.
Returns a _ProjectApiKeys containing the anon key and the service
role key. The service role key is required to:
- Populate the
SUPABASE_SERVICE_ROLE_KEYEdge Function secret soai-proxycan read/write protected tables. - Write the functions bundle hash into
_schema_meta.functions_hash(via a short-lived PostgREST call) so the incremental update detector in SchemaMigrator can surface drift.
The service role key itself is NEVER persisted by the app — it is held in memory for the bootstrap flow and dropped when the setup dialog closes.
Implementation
Future<ProjectApiKeys> fetchApiKeys({
required String projectRef,
required String accessToken,
}) async {
final url = '$_baseUrl/v1/projects/$projectRef/api-keys?reveal=true';
final Response<dynamic> response;
try {
response = await _dio.get<dynamic>(
url,
options: Options(
headers: _authHeaders(accessToken),
sendTimeout: _timeout,
receiveTimeout: _timeout,
validateStatus: (_) => true,
),
);
} on DioException catch (e) {
throw ProvisioningException(_dioMessage(e, 'fetching project API keys'));
}
final status = response.statusCode ?? 0;
if (status < 200 || status >= 300) {
throw ProvisioningException(
_mapStatus(status, projectRef, response.data),
);
}
final data = response.data;
if (data is! List) {
throw ProvisioningException(
'Unexpected api-keys response shape from Supabase.',
);
}
String? anon;
String? serviceRole;
for (final entry in data) {
if (entry is! Map) continue;
final name = entry['name'] as String?;
final key = entry['api_key'] as String?;
if (key == null || key.isEmpty) continue;
if (name == 'anon') anon = key;
if (name == 'service_role') serviceRole = key;
}
if (serviceRole == null) {
throw ProvisioningException(
'Supabase did not return a service_role key — make sure the access '
'token has the "secrets:read" scope.',
);
}
return ProjectApiKeys(anonKey: anon ?? '', serviceRoleKey: serviceRole);
}