Upload files as AT Protocol records to a PDS. Content-neutral design works with any AT Protocol collection and content type.
Built with atcute and Deno.
# Run directly from JSR
deno run -A jsr:@fry69/putrecord init
# Configure your credentials
cp .env.example .env
# Edit .env with your PDS credentials
# Upload a file
deno run -A jsr:@fry69/putrecord# Run directly
deno run -A jsr:@fry69/putrecord [OPTIONS]
# Available commands
deno run -A jsr:@fry69/putrecord init # Initialize project files
deno run -A jsr:@fry69/putrecord --help # Show helpimport {
buildRecord,
createRecord,
loadConfig,
readFile,
uploadRecord,
} from "jsr:@fry69/putrecord/lib";
const config = loadConfig();
const content = await readFile(config.filePath);
const record = buildRecord(config.collection, content);
const result = await createRecord(client, config, record);See Library API documentation for details.
Create a .env file with your credentials:
PDS_URL=https://bsky.social
IDENTIFIER=alice.bsky.social
APP_PASSWORD=xxxx-xxxx-xxxx-xxxx
COLLECTION=com.example.note
FILE_PATH=./content/note.txt
# RKEY= # Optional: omit for create, set for updateEnvironment Variables:
PDS_URL- PDS endpointIDENTIFIER- Your handle or DIDAPP_PASSWORD- App password (not main password!)COLLECTION- Collection in NSID formatFILE_PATH- Path to file to uploadRKEY- Optional: omit to create new record, set to update existing
Omit RKEY to create a new record. The PDS generates a timestamp-based RKEY
that you can save for updates.
# First upload - no RKEY in .env
deno run -A jsr:@fry69/putrecord
# Output shows:
# ⚠️ Save this RKEY for future updates: 3l4k2j3h4k5l
# Add to your .env file: RKEY=3l4k2j3h4k5lInclude RKEY in your .env to update the existing record.
# Add RKEY to .env
echo "RKEY=3l4k2j3h4k5l" >> .env
# Updates will now modify the existing record
deno run -A jsr:@fry69/putrecordWorks with any content type:
- JSON with
$type: Used as-is for custom lexicon schemas - WhiteWind blog entries (
com.whtwnd.blog.entry): Automatically creates proper blog records withvisibility: "public"and extracts title from markdown - Plain text: Wrapped in a generic record structure with
$type,content, andcreatedAt
WhiteWind Blog Example:
# My Blog Post
This is my blog content in markdown format.Automatically becomes:
{
"$type": "com.whtwnd.blog.entry",
"content": "# My Blog Post\n\nThis is my blog content...",
"title": "My Blog Post",
"visibility": "public",
"createdAt": "2025-10-08T..."
}Important: When updating existing WhiteWind records, putrecord preserves
your custom title and visibility fields. Use --force-fields to override
and always extract the title from markdown.
Custom JSON Example:
{
"$type": "com.example.myapp.post",
"title": "My Post",
"content": "Post content here",
"tags": ["tag1", "tag2"]
}-q, --quiet- Suppress non-error output-f, --force- Overwrite existing files (init command)--force-fields- Force extraction of fields (title, visibility) from content, overwriting existing values in update mode-h, --help- Show help-v, --version- Show version
The init command sets up everything you need:
# Initialize GitHub Actions workflow
deno run -A jsr:@fry69/putrecord initThis creates:
.github/workflows/putrecord.yaml- Automated upload workflow.env.example- Configuration template
Setup secrets:
# Using GitHub CLI
gh secret set -f .env
# Or manually: Settings → Secrets and variables → ActionsSee GitHub Actions Guide for detailed setup.
See putrecord-test - A working example that demonstrates automated uploads using GitHub Actions. The repository automatically updates its README on every push and uploads it to a PDS.
- Library API - Programmatic usage and function reference
- GitHub Actions - CI/CD automation setup
- Development - Testing and contributing
- Content Publishing: Automate blog post or note uploads
- Data Sync: Keep PDS records in sync with repository files
- CI/CD Integration: Deploy content changes automatically
- Custom Applications: Build tools using any AT Protocol collection
MIT