| title | Scripts And App Passwords |
|---|---|
| description | Use Poptart from trusted scripts, cron jobs, and internal tools with app-password sessions. |
For scripts and internal tools that you run yourself, app-password sessions are the simplest way to call authenticated AT Protocol endpoints. They avoid browser redirect handling and work well with generated descriptors.
Use this flow for cron jobs, moderation helpers, migration scripts, and local admin tools. Use OAuth instead for user-facing apps.
dart pub add poptartimport 'package:poptart/poptart.dart';Do not hard-code credentials in the script. Read the identifier and app password from environment variables or your secret manager.
import 'dart:io';
String requiredEnv(String name) {
final value = Platform.environment[name];
if (value == null || value.isEmpty) {
throw StateError('Missing required environment variable: $name');
}
return value;
}Run the script with values like:
BSKY_IDENTIFIER=alice.example.com BSKY_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx dart run bin/tool.dartUse createSession to exchange the identifier and app password for an
AT Protocol session.
Future<Session> createScriptSession() async {
final response = await createSession(
identifier: requiredEnv('BSKY_IDENTIFIER'),
password: requiredEnv('BSKY_APP_PASSWORD'),
);
return response.data;
}The returned Session includes the DID, handle, access JWT, and refresh JWT.
Generated method values and descriptors keep the script typed without making it verbose.
import 'dart:io';
import 'package:poptart/poptart.dart';
import 'package:bluesky_poptart/app/bsky/notification/list_notifications.dart'
as notifications;
Future<void> main() async {
final session = await createScriptSession();
final client = PoptartClient.fromSession(session);
final response = await client.call(
notifications.appBskyNotificationListNotifications,
parameters: const notifications.NotificationListNotificationsInput(
limit: 50,
),
);
for (final notification in response.data.notifications) {
stdout.writeln(
'${notification.reason}: ${notification.author.handle}',
);
}
}Prefer generated lexicon calls for known methods. The named method value is the
friendly path; notifications.methodDescriptor is equally valid when the
descriptor reads better in a script. Both give typed parameters, typed output,
and less response-shape guessing.
Repo writes use the same authenticated client. The generated method decides which lexicon method is called.
import 'package:poptart/poptart.dart';
import 'package:bluesky_poptart/app/bsky/feed/post.dart' as feed_post;
import 'package:poptart_lex/com/atproto/repo/create_record.dart'
as create_record;
Future<void> publishPost(Session session, String text) async {
final client = PoptartClient.fromSession(session);
final post = feed_post.FeedPostRecord(
text: text,
createdAt: DateTime.now().toUtc(),
);
await client.call(
create_record.comAtprotoRepoCreateRecord,
input: create_record.RepoCreateRecordInput(
repo: session.did,
collection: 'app.bsky.feed.post',
record: post.toJson(),
),
);
}Build records with generated models when possible, then pass toJson() to repo
write inputs.
For short scripts, create a fresh session each run. For longer-running jobs, refresh the session before the access token expires.
Future<Session> refreshScriptSession(Session session) async {
final refreshed = await refreshSession(refreshJwt: session.refreshJwt);
return refreshed.data;
}If your process keeps running for hours, store the latest refresh JWT after each
refresh and replace the in-memory Session.
Wrap the top-level script so failures are explicit and exit non-zero.
Future<void> main() async {
try {
final session = await createScriptSession();
await publishPost(session, 'Hello from a Poptart script');
} catch (error, stack) {
stderr.writeln(error);
stderr.writeln(stack);
exitCode = 1;
}
}For scheduled jobs, log enough context to know which account and method failed, but never log app passwords, access JWTs, refresh JWTs, or OAuth private keys.
Do not ask users to type their password or app password into a shipped app. Use OAuth for that.
Do not use account passwords for scripts. Create an app password with only the access needed for the job.
Do not store generated sessions in world-readable files. Treat session material as credentials.