atproto_core

atproto_core is the request layer that atproto and bluesky are built on. It owns ServiceContext — the object that resolves which host a call goes to, attaches credentials, refreshes an expired session, and runs the retry loop — plus the session, JWT, blob, and retry types those clients expose.
Most readers should not add it to their pubspec.yaml. You are probably here because a type from this package showed up in your code (Session, XRPCResponse, RetryConfig, UnauthorizedException), and you want to know where it comes from and how to name it.
Use atproto_core directly for:
decodeCar— decoding a repository export or firehose block intoCID -> blockBaseHttpService— a minimal non-XRPC HTTP base class for your own service client- The default constants (
defaultService,defaultRelayService,defaultTimeout,defaultProtocol,defaultLinkPreviewService)
Look elsewhere for:
- Calling
com.atproto.*endpoints — use atproto - Calling
app.bsky.*/chat.bsky.*endpoints — use bluesky - Naming
Session,XRPCResponse,RetryStrategy, or the XRPC exceptions — importpackage:atproto/core.dartorpackage:bluesky/core.dartinstead (see below) - Speaking XRPC against a service with no generated client — use xrpc
AtUriandNSIDthemselves — use at_primitives
You already have these types
atproto and bluesky re-export the part of this package that reaches your code through a dedicated library:
import 'package:bluesky/core.dart' as core; // or package:atproto/core.dart
package:bluesky/core.dart simply re-exports package:atproto/core.dart, so the two are interchangeable. Prefixing the import with as core is the convention used throughout these docs, because names like Blob and Session are generic enough to collide with your own.
import 'package:bluesky/app_bsky_feed_gettimeline.dart';
import 'package:bluesky/bluesky.dart';
import 'package:bluesky/core.dart' as core;
Future<void> main() async {
// `Session` and `RetryStrategy` are `atproto_core` types, but they are
// reachable through `package:bluesky/core.dart` without adding
// `atproto_core` to your pubspec.
final core.Session session = await _restoreSession();
final core.RetryStrategy retry = core.RetryConfig(maxAttempts: 3);
final bsky = Bluesky.fromSession(session, retryConfig: retry);
// Every lexicon method returns an `XRPCResponse<T>`: `data` is the typed
// payload, `headers` and `rateLimit` describe the response itself.
final core.XRPCResponse<FeedGetTimelineOutput> response = await bsky.feed
.getTimeline(limit: 25);
print('Posts: ${response.data.feed.length}');
print('Remaining: ${response.rateLimit.remainingCount}');
}
Future<core.Session> _restoreSession() async => throw UnimplementedError();
That library exposes ServiceContext, Session (plus SessionExtension), Jwt, decodeJwt, isValidAppPassword, XRPCResponse, XRPCRequest, XRPCError, RateLimit, RateLimitPolicy, Subscription, Protocol, HttpMethod, HttpStatus, EmptyData, Serializable, ResponseDataBuilder, ResponseDataAdaptor, GetClient, PostClient, NSID, AtUri, CID, Multicodec, InvalidCidError, Blob, BlobRef, BlobConverter, the retry types, and the XRPC exception hierarchy. If your type is in that list, you do not need a direct dependency.
What is actually in the package
ServiceContext
ServiceContext is the single object every generated lexicon method calls through. ATProto and Bluesky construct one for you; you never build one yourself in application code. It is worth knowing what it does, because the behaviour is easy to mistake for magic:
- Host resolution is lazy, re-evaluated on every request. Precedence is an explicit
service:override, then the session's PDS endpoint (from the#atproto_pdsservice indidDoc, falling back to the access token'saudclaim), then the OAuth manager's current PDS host, thenbsky.social. An OAuth session that materializes or moves to a different PDS after the client was built is therefore picked up on the next call. - Session refresh happens both proactively (the access token is decoded and refreshed 30 seconds before it expires) and reactively (on a genuine
401). Concurrent refreshes are deduplicated into one in-flight call, so N simultaneous expired requests do not trigger N refresh POSTs. - A caller-supplied
Authorizationheader always wins. It is matched case-insensitively and is never overwritten by the session token or by an OAuth DPoP header — this is what makes service-auth tokens (for example the video service) work. - Retries, the DPoP nonce handshake, and the
401refresh all run inside the same wrapper around each request.
See Authentication & Sessions for how to obtain and persist a Session, and Custom Services for pointing a client at a different host.
Sessions and JWTs
Session is a freezed class carrying did, handle, accessJwt, refreshJwt, didDoc, and the email/status flags. Two details matter in practice:
- Its
toString()redacts both JWTs, so printing or crash-reporting aSessiondoes not leak credentials. SessionExtensionaddsaccessTokenJwt,refreshTokenJwt, andatprotoPdsEndpoint.
decodeJwt returns a Jwt and throws a FormatException on malformed input; JwtExtension adds isExpired and remainingTime. There is no signature verification here — decoding a token you received over TLS is not the same as validating one you were handed.
OAuthSession, OAuthSessionManager, and OAuthSessionRevokedException are re-exported from atproto_oauth, and package:atproto_core/atproto_oauth.dart forwards that package wholesale.
The retry system
RetryStrategy is a one-method interface (nextDelay(RetryContext)) and RetryConfig is the default implementation: capped exponential backoff with jitter, honouring a server-sent Retry-After as a lower bound capped at 60 seconds. RetryContext tells a strategy the attempt number, the normalized RetryReason, whether the request was a procedure, and whether the failure was ambiguous — which is why a POST is not retried after a timeout or 5xx by default.
Everything about configuring and replacing this is in Retries & Timeouts.
Errors
The XRPC exception hierarchy (XRPCException, UnauthorizedException, InvalidRequestException, RateLimitExceededException, InternalServerErrorException, XRPCNotSupportedException) is defined in xrpc and re-exported here unchanged. Catching and interpreting them is covered in Error Handling.
HTTP client injection
GetClient and PostClient are typedefs from xrpc, threaded through ServiceContext to every request. See Custom HTTP Clients.
Data model helpers
Blob and BlobRef model the blob type; BlobConverter also accepts the legacy {cid, mimeType} shape and normalizes it. AtUriConverter and NsidConverter are JsonConverters used by generated code. objectType is the $type key constant. NSIDConverter is a deprecated alias of NsidConverter.
Decoding CAR files
decodeCar is the main reason to take a direct dependency: it is not re-exported by atproto or bluesky. It turns a CAR payload into Map<String, Map<String, dynamic>> keyed by CID string, normalizing DAG-CBOR CID links to {"$link": "..."} and raw bytes to {"$bytes": "..."} per the atproto data model. Truncated or malformed input throws CarException rather than a raw RangeError.
import 'package:atproto/atproto.dart';
// `decodeCar` is not re-exported by `atproto`/`bluesky`, so this import
// requires `atproto_core` in your own pubspec.
import 'package:atproto_core/atproto_core.dart' show decodeCar, CarException;
Future<void> main() async {
final atproto = ATProto.anonymous();
// A repository export arrives as raw CAR bytes.
final export = await atproto.sync.getRepo(
did: 'did:plc:iijrtk7ocored6zuziwmqq3c',
);
try {
// `CID string -> decoded block`.
final blocks = decodeCar(export.data);
for (final entry in blocks.entries) {
print('${entry.key}: ${entry.value[r'$type'] ?? '<commit or node>'}');
}
} on CarException catch (e) {
// Truncated or malformed input, rather than a raw RangeError.
print('Could not decode the export: ${e.message}');
}
}
For firehose frames you almost never need this directly — the adaptors in package:atproto/firehose.dart decode commit blocks for you. See the Firehose guide.
Related Packages
| Package | Use it for |
|---|---|
| atproto | com.atproto.* endpoints; re-exports this package's public types via package:atproto/core.dart |
| bluesky | app.bsky.* and chat.bsky.* endpoints |
| xrpc | The XRPC layer below this package: requests, responses, and the exception hierarchy |
| at_primitives | AtUri and NSID, re-exported here |
| atproto_oauth | OAuth sessions and DPoP, re-exported here |