R2 Artifact Storage
Documentation for Cloudflare R2 artifact storage used by the TinyKit CLI for distributing product tarballs.
The TinyKit CLI uses Cloudflare R2 for storing and distributing product artifacts (tinykit-lite, tinykit-pro, tinykit-dirs). R2 provides S3-compatible storage with generous free tier and no egress fees.
Bucket Structure
TinyKit products are organized in a single R2 bucket with the following structure:
tinykit-artifacts/
├── tinykit-lite/
│ ├── 1.0.0.tar.gz
│ ├── 1.0.1.tar.gz
│ └── latest.json
├── tinykit-pro/
│ ├── 2.0.0.tar.gz
│ ├── 2.1.0.tar.gz
│ └── latest.json
└── tinykit-dirs/
├── 1.0.0.tar.gz
└── latest.jsonDirectory Layout
Each product has its own directory containing:
- Version tarballs (
{version}.tar.gz): Compressed archives of the product - Latest manifest (
latest.json): Points to the current latest version
Latest Manifest Format
The latest.json file in each product directory contains:
{
"version": "2.1.0",
"uploadedAt": 1706400000000
}| Field | Type | Description |
|---|---|---|
version | string | Semantic version of the latest release |
uploadedAt | number | Unix timestamp (ms) when this version was uploaded |
Environment Variables
Configure R2 access by setting these Convex environment variables:
npx convex env set R2_ACCOUNT_ID your_cloudflare_account_id
npx convex env set R2_ACCESS_KEY_ID your_r2_access_key
npx convex env set R2_SECRET_ACCESS_KEY your_r2_secret_key
npx convex env set R2_BUCKET_NAME tinykit-artifactsGetting R2 Credentials
- Account ID: Find in the Cloudflare Dashboard URL or R2 overview page
- API Tokens: Create in Cloudflare Dashboard > R2 > Manage R2 API Tokens
- Permission: "Object Read & Write" for the bucket
- This generates both Access Key ID and Secret Access Key
Code Integration
Generating Signed Download URLs
The R2 utilities in convex/lib/r2.ts provide signed URL generation:
import { generateSignedDownloadUrl, isR2Configured } from "../lib/r2";
// Check if R2 is configured
if (!isR2Configured()) {
throw new Error("Artifact storage not configured");
}
// Generate a signed URL (expires in 15 minutes)
const downloadUrl = await generateSignedDownloadUrl("tinykit-pro", "2.1.0");Checking Configuration
import { isR2Configured, getR2BucketName } from "../lib/r2";
// Check availability before attempting downloads
if (isR2Configured()) {
console.log(`Using bucket: ${getR2BucketName()}`);
}Artifact Types
ProductArtifact
Metadata for a versioned product release:
interface ProductArtifact {
product: "tinykit-lite" | "tinykit-pro" | "tinykit-dirs";
version: string; // e.g., "2.1.0"
filename: string; // e.g., "2.1.0.tar.gz"
size: number; // bytes
checksum: string; // "sha256:abc123..."
uploadedAt: number; // Unix timestamp (ms)
}LatestManifest
Points to the current latest version:
interface LatestManifest {
version: string; // e.g., "2.1.0"
uploadedAt: number; // Unix timestamp (ms)
}Security
Signed URLs
All download URLs are:
- Time-limited: Expire after 15 minutes
- Single-use friendly: Can be used multiple times within expiry window
- Authenticated: Require valid R2 credentials to generate
Access Control
The Convex backend verifies user permissions before generating download URLs:
- User authenticates via CLI
- Backend checks subscription status
- If authorized, generates signed URL
- URL is valid for 15 minutes
Release Pipeline
Artifacts are uploaded via GitHub Actions when tagging a release:
# .github/workflows/release.yml
name: Release to R2
on:
push:
tags:
- "v*"
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Filter files via .tinykitignore
run: |
# Exclude internal files from release
- name: Create tarball
run: tar -czf release.tar.gz .
- name: Upload to R2
uses: cloudflare/wrangler-action@v3
with:
command: r2 object put tinykit-artifacts/${{ env.PRODUCT }}/${{ env.VERSION }}.tar.gz --file=release.tar.gz
- name: Update latest.json
run: |
echo '{"version": "${{ env.VERSION }}", "uploadedAt": '${{ github.event.head_commit.timestamp }}'}' > latest.json
wrangler r2 object put tinykit-artifacts/${{ env.PRODUCT }}/latest.json --file=latest.jsonTroubleshooting
R2 Not Configured
If downloads fail with "R2 not configured":
- Verify all environment variables are set:
npx convex env list | grep R2 - Check for typos in variable names
- Ensure the API token has correct permissions
Download URL Expired
Signed URLs expire after 15 minutes. If a download fails:
- Request a new download URL from the CLI
- Start download immediately after receiving URL
Permission Denied
If uploads fail in CI:
- Verify R2 API token has "Object Read & Write" permission
- Check bucket name matches exactly
- Ensure CORS settings allow uploads if using browser
Best Practices
- Version naming: Use semantic versioning (e.g., "1.0.0", "2.1.0-beta.1")
- Checksums: Always include sha256 checksums for integrity verification
- Cleanup: Periodically remove old versions to manage storage costs
- Monitoring: Track download metrics for usage analytics
Hono + OpenAPI Integration
TinyKit Pro uses Hono with @hono/zod-openapi for HTTP endpoints, providing automatic OpenAPI specification generation, interactive API documentation, and typ...
Automated Versioning & Release Management
TinyKit Pro uses automated semantic versioning with conventional commits to streamline release management and maintain accurate changelogs.