feat(powersync): add SyncOptions.headers for custom sync HTTP headers#420
feat(powersync): add SyncOptions.headers for custom sync HTTP headers#420xapple wants to merge 1 commit into
Conversation
simolus3
left a comment
There was a problem hiding this comment.
Thank you for your contribution! I think this is a helpful feature to add. I wonder if these static headers set on SyncOptions are the best way to expose this, though. Alternatively, we could add an optional field on PowerSyncCredentials to provide custom headers. This would have the benefit of being able to generate fresh headers for each auth session (in case that's relevant). What do you think?
Internally, we might have to discuss whether this needs alignment with our other SDKs, but I'm leaning towards this being fine as-is. In Kotlin, Swift, and JS, the same thing is already possible through a custom http client or AbstractRemote implementation. We can't really let users inject their own http clients here since the sync process runs on a background isolate / worker, so adding this via a new option doesn't sound that risky to me.
| external String? get syncParamsEncoded; | ||
| external UpdateSubscriptions? get subscriptions; | ||
| external String? get appMetadataEncoded; | ||
| external String? get headersEncoded; |
There was a problem hiding this comment.
FWIW we might just want to use a plain JSObject for this, encoding key-value pair as JS fields using dart:js_interop_unsafe. But that shouldn't make a huge difference.
|
Yes, having a field on |
|
We've discussed this internally, and we have an update on how we probably want to expose this. Both Adding custom headers is essentially a network-level interceptor, and that's the way we expose that on our other SDKs. For example, our Kotlin SDK has this, which allows configuring the ktor client or providing a custom one. Designing these APIs to be similar to what we have for Kotlin could be a good solution. What I'm thinking is essentially this:
To add custom headers, you can write a client extending I think this gives users maximum flexibility, and could also be interesting if they want to use a custom (e.g. FFI based) http client with PowerSync. |
|
I can see the factory approach is architecturally cleaner and cross-SDK idiomatic, but it's worse ergonomically for the common simple headers use case. In particular, web parity isn't really in scope as proposed: how much work would be left to the developer to get this working seamlessly? In my view, requiring devs to compile a custom worker for web clients is a significant build-step regression. Your call. |
|
We run PowerSync self-hosted behind TLS signed by our own private root CA (installed in the Android user trust store). On Android the sync connection fails with Two questions:
|
That's true, but I feel a custom worker is also the cleanest solution to this. Of course we could "just add support for custom headers" since it's probably the most common request, but then we might get similar requests in the future. What if you need to attach a query parameter instead, or if you have a setup where one request header is essentially a signature of all the others (I've seen that happen!)? It's better to get the architecture right now, and then figure out what we can do to safely improve DX afterwards. For instance, this page mentions that |
Yes, that's the idea 👍
Now we're getting to these very reasonable requests which would completely blow up the complexity of our APIs :) This is why I think a custom HTTP client is the best API for this, it gives everyone maximum flexibility. |
|
@xapple, I'm also happy to take a look at this based on your initial work (I'll try some options to support an http client factory without custom workers too). |
|
I suppose that having both solutions developed in parallel should not be an issue. A user could then go the quick&easy route for just a simple auth token in the headers, and use the more complex HTTP client factory when certificates are needed etc. |
Given that we will have a general-purpose approach for this soon (#424), I don't think it makes sense to add headers as a separate API. We can't remove that functionality later, and we'd have to align that feature with all other SDKs which is a lot of effort for a use case also served by custom http clients. Writing a custom client to add headers is not that hard: final class AddHeadersClient extends BaseClient {
final Client _inner;
final Map<String, String> _additionalHeaders;
AddHeadersClient(
{Client? inner, required Map<String, String> additionalHeaders})
: _inner = inner ?? Client(),
_additionalHeaders = additionalHeaders;
@override
Future<StreamedResponse> send(BaseRequest request) async {
_additionalHeaders.forEach((k, v) => request.headers[k] = v);
return _inner.send(request);
}
@override
void close() => _inner.close();
}I don't think the ergonomics of this are bad enough to justify an API to make this particular usecase easier. Anyway, thank you for your contribution and starting this discussion! |
|
Sure, happy to contribute. I'm not sure I understand the full scope correctly, as I'm still learning, but it seems like this solution solves the "closure-can't-cross-postMessage problem" with a proxy architecture. So it would in fact work on the web client seamlessly (and no recompiling the worker). Well done! |
Adds
SyncOptions.headersaMap<String, String>?of HTTP headers attached to every request the sync client makes to the PowerSync service.Currently, the sync HTTP client is constructed internally with no header-injection hook (
http.Client()andBrowserClient()are hardcoded). That blocks deployments behind corporate proxies, zero-trust gateways, or anything requiring per-request middleware (like Cloudflare Access).The Supabase Flutter SDK already supports this via
Supabase.initialize(headers: ...), but PowerSync has no equivalent.A Client Function() factory would be more flexible (cert pinning, retry policy, interceptors) but closures cannot cross the web worker postMessage boundary.
No public API breaking changes, the field is optional and defaults to null. The
DevConnectorAPI calls inlib/src/connector.dartare out of scope.I also let Claude add a couple tests to the PR. Tell me what you think. Thanks.