TinyKit Pro Docs

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.json

Directory 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
}
FieldTypeDescription
versionstringSemantic version of the latest release
uploadedAtnumberUnix 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-artifacts

Getting R2 Credentials

  1. Account ID: Find in the Cloudflare Dashboard URL or R2 overview page
  2. 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:

  1. User authenticates via CLI
  2. Backend checks subscription status
  3. If authorized, generates signed URL
  4. 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.json

Troubleshooting

R2 Not Configured

If downloads fail with "R2 not configured":

  1. Verify all environment variables are set:
    npx convex env list | grep R2
  2. Check for typos in variable names
  3. Ensure the API token has correct permissions

Download URL Expired

Signed URLs expire after 15 minutes. If a download fails:

  1. Request a new download URL from the CLI
  2. Start download immediately after receiving URL

Permission Denied

If uploads fail in CI:

  1. Verify R2 API token has "Object Read & Write" permission
  2. Check bucket name matches exactly
  3. Ensure CORS settings allow uploads if using browser

Best Practices

  1. Version naming: Use semantic versioning (e.g., "1.0.0", "2.1.0-beta.1")
  2. Checksums: Always include sha256 checksums for integrity verification
  3. Cleanup: Periodically remove old versions to manage storage costs
  4. Monitoring: Track download metrics for usage analytics

On this page

Ship your startup faster. In minutes.

Get TinyKit Pro