Product Requirements Document: artidrop

Version: 1.2 | Date: 2026-03-22


1. Problem Statement

AI agents (Claude, ChatGPT, LangChain pipelines, custom agents) routinely generate rich artifacts -- HTML pages, Markdown reports, interactive visualizations, dashboards -- but there is no simple, universal way to publish these outputs and get a shareable URL.

Today, users must either:

Agents themselves have no built-in "publish" primitive. No major agent framework (LangChain, CrewAI, AutoGen, Google ADK) offers a way to turn an output into a live URL.

2. Product Vision

artidrop is the publishing layer for AI agents. One command, one API call, or one drag-and-drop -- and any artifact gets a live, shareable URL.

For consumers: drag and drop an artifact, get a link to share. For developers: a CLI tool and SDK that agents call to publish automatically. For teams: a dashboard to manage, version, and analyze all artifacts your agents produce.

3. Target Users

Primary: Developers building AI agents

Secondary: AI power users (non-developers)

Tertiary: Teams and organizations

4. User Journeys

Journey 1: Consumer drag-and-drop

  1. User generates an HTML artifact in Claude / ChatGPT / any AI tool
  2. User saves the file locally (or copies the HTML)
  3. User opens artidrop.app, signs in with Google (one click)
  4. User drags the file onto the page (or pastes HTML)
  5. artidrop returns a live URL (e.g., artidrop.app/a/x7k9m2)
  6. User shares the URL -- recipient sees the rendered artifact immediately

Journey 2: Developer CLI publish

  1. Developer installs: npm install -g artidrop (or pip install artidrop)
  2. Agent generates an HTML file at ./output/report.html
  3. Agent runs: artidrop publish ./output/report.html
  4. CLI prints: Published: https://artidrop.app/a/x7k9m2
  5. Agent includes the URL in its response to the user, sends it via Slack, emails it, etc.
  6. First use requires artidrop login or setting ARTIDROP_API_KEY env var.

Journey 3: Agent SDK integration

from artidrop import Artidrop

client = Artidrop(api_key="sk-...")
result = client.publish(
    content="<html>...<h1>Q1 Revenue Report</h1>...</html>",
    title="Q1 Revenue Report",
    format="html",
)
print(result.url)  # https://artidrop.app/a/x7k9m2

Journey 4: MCP tool (agent discovers and uses artidrop)

  1. User configures artidrop MCP server in Claude Code, Cursor, or another MCP-aware client
  2. During a conversation, the agent generates an artifact and decides to publish it
  3. Agent calls the artidrop_publish MCP tool with the HTML content
  4. Tool returns the live URL
  5. Agent presents the URL to the user

Journey 5: Team dashboard

  1. Team admin creates an artidrop workspace, invites team members
  2. Multiple agents across the team publish artifacts using workspace API keys
  3. All artifacts appear in the team dashboard -- searchable, filterable by agent/date/tag
  4. Admin sets a custom domain (reports.company.com) for published artifacts
  5. Team members can view analytics (views, unique visitors) per artifact

5. Phase 1 Features (MVP) — Detailed Specification

The minimum product that delivers value and validates the concept. Phase 1 ships three surfaces (web UI, REST API, CLI) backed by a single API server with artifact storage, rendering, versioning, and authentication.


F1. Web Upload UI

F1.1 Landing page

The artidrop.app homepage is the primary onboarding surface. It has one job: turn a file or pasted content into a live URL with as little friction as possible.

Layout (top to bottom):

  1. Header bar — logo, tagline ("Instant shareable URLs for AI artifacts"), "Sign in with Google" button (top-right). If authenticated: user avatar, "My Artifacts" link, Sign Out.
  2. Drop zone (visible only when signed in) — large centered area (minimum 400x300px) with dashed border. States:
    • Default: icon + "Drop an HTML or Markdown file here, or click to browse". Below the zone: "or paste content" toggle.
    • Hover (file dragged over): border turns solid blue, background lightens, text changes to "Drop to publish".
    • Uploading: spinner with "Publishing..." text.
    • Success: shows the published URL with a one-click copy button, "Open" link, and QR code. Below: "Preview" iframe showing the rendered artifact.
    • Error: red border, error message (e.g., "File too large (max 10MB)", "Unsupported file type").
  3. Paste mode — toggling "or paste content" reveals a code editor area (monospace, line numbers, syntax highlighting via lightweight library like CodeMirror). Tab toggle: HTML | Markdown. "Publish" button below the editor.
  4. Signed-out state — when not signed in, the drop zone is replaced by a hero section explaining the product with a prominent "Sign in with Google" CTA. A sample artifact preview can be shown below as social proof.
  5. Footer — links: Docs, API, CLI, GitHub, Terms, Privacy.

F1.2 Accepted inputs

Input Method Behavior
Single .html file Drag-and-drop or file picker Publish as HTML artifact
Single .htm file Drag-and-drop or file picker Publish as HTML artifact
Single .md file Drag-and-drop or file picker Publish as Markdown artifact (rendered to HTML at upload time)
Single .markdown file Drag-and-drop or file picker Same as .md
Pasted HTML string Paste mode Publish as HTML artifact. If the string is not wrapped in <html> or <!DOCTYPE, wrap it in a minimal HTML shell
Pasted Markdown string Paste mode (Markdown tab) Publish as Markdown artifact

Not accepted in Phase 1 (deferred):

F1.3 Validation rules

Rule Limit Error message
Max file size 10MB "File exceeds the 10MB size limit."
Max paste length 2MB (character count) "Pasted content exceeds the 2MB limit."
Allowed file extensions .html, .htm, .md, .markdown "Unsupported file type. We accept HTML and Markdown files."
Empty content 0 bytes "The file is empty."
Rate limit 60 publishes per hour per API key "Rate limit reached. Try again in {minutes} minutes."

F1.4 Acceptance criteria


F2. REST API

The API is the single source of truth. The web UI and CLI are both clients of this API.

Base URL: https://api.artidrop.app/v1

F2.1 Authentication

All requests are authenticated via Bearer token in the Authorization header, except:

Authorization: Bearer sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

All write operations (POST, PUT, DELETE) require authentication. Requests are rate-limited by API key.

F2.2 Endpoints

POST /v1/artifacts — Create artifact

Creates a new artifact and returns its metadata including the public URL.

Request:

{
  "content": "<html><body><h1>Hello World</h1></body></html>",
  "format": "html",
  "title": "My Report",
  "visibility": "public"
}
Field Type Required Default Description
content string Yes The artifact content (raw HTML or Markdown)
format string Yes "html" or "markdown"
title string No "Untitled" Display title (max 200 chars)
visibility string No "public" "public" or "unlisted". ("private" deferred to Phase 3)

Response (201 Created):

{
  "id": "art_x7k9m2p4",
  "url": "https://artidrop.app/a/x7k9m2",
  "title": "My Report",
  "format": "html",
  "visibility": "public",
  "version": 1,
  "size_bytes": 2048,
  "created_at": "2026-03-22T10:00:00Z",
  "updated_at": "2026-03-22T10:00:00Z",
  "owner": {
    "id": "usr_abc123",
    "username": "wen"
  }
}

Content wrapping: If format is "html" and the content does not contain <html or <!doctype (case-insensitive), the server wraps it:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{title}</title>
</head>
<body>
{content}
</body>
</html>
GET /v1/artifacts/:id — Get artifact metadata

Returns metadata only (not content). Use the url field or /a/:id/raw to fetch content.

Response (200 OK):

{
  "id": "art_x7k9m2p4",
  "url": "https://artidrop.app/a/x7k9m2",
  "title": "My Report",
  "format": "html",
  "visibility": "public",
  "version": 3,
  "size_bytes": 2048,
  "created_at": "2026-03-22T10:00:00Z",
  "updated_at": "2026-03-23T14:00:00Z",
  "owner": {
    "id": "usr_abc123",
    "username": "wen"
  }
}

Access rules:

GET /v1/artifacts — List artifacts

Returns the authenticated user's artifacts, paginated.

Query parameters:

Param Type Default Description
limit integer 20 Items per page (max 100)
offset integer 0 Pagination offset
format string Filter by "html" or "markdown"
sort string "created_at" "created_at", "updated_at", "title"
order string "desc" "asc" or "desc"

Response (200 OK):

{
  "items": [ /* array of artifact objects */ ],
  "total": 42,
  "limit": 20,
  "offset": 0
}

Requires authentication. Returns 401 if unauthenticated.

PUT /v1/artifacts/:id — Update artifact

Replaces the artifact content, creating a new version. Only the owner can update.

Request:

{
  "content": "<html><body><h1>Updated Report</h1></body></html>",
  "title": "My Updated Report"
}
Field Type Required Description
content string No New content. If omitted, content is unchanged (metadata-only update).
format string No Cannot change format after creation. Returns 400 if provided and different from original.
title string No New title
visibility string No Change visibility

If content is provided, version increments by 1. If only metadata fields change, version stays the same.

Response (200 OK): Updated artifact object.

DELETE /v1/artifacts/:id — Delete artifact

Permanently deletes the artifact and all its versions. Only the owner can delete.

Response (204 No Content)

GET /v1/artifacts/:id/versions — List versions

Returns version history for an artifact.

Response (200 OK):

{
  "items": [
    {
      "version": 3,
      "size_bytes": 3072,
      "created_at": "2026-03-23T14:00:00Z"
    },
    {
      "version": 2,
      "size_bytes": 2560,
      "created_at": "2026-03-22T18:00:00Z"
    },
    {
      "version": 1,
      "size_bytes": 2048,
      "created_at": "2026-03-22T10:00:00Z"
    }
  ],
  "total": 3
}

Access rules: same as GET /v1/artifacts/:id.

F2.3 Error responses

All errors follow a consistent shape:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Content exceeds the maximum size of 10MB.",
    "details": {
      "field": "content",
      "max_bytes": 10485760,
      "actual_bytes": 15000000
    }
  }
}
HTTP Status Error Code When
400 VALIDATION_ERROR Invalid input (missing required field, bad format, content too large, title too long)
400 FORMAT_CHANGE_NOT_ALLOWED Attempting to change artifact format on update
401 UNAUTHORIZED Missing or invalid API key
403 FORBIDDEN Authenticated but not the owner of this artifact
404 NOT_FOUND Artifact ID does not exist
409 CONFLICT Slug already taken (Phase 2, but reserve the error code)
429 RATE_LIMITED Too many requests. Response includes Retry-After header (seconds).
500 INTERNAL_ERROR Server error

F2.4 Rate limiting headers

Every response includes:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
X-RateLimit-Reset: 1711108800

F3. CLI Tool

F3.1 Installation

npm install -g artidrop

The CLI is a single npm package. No native dependencies. Requires Node.js >= 18.

F3.2 Authentication

The CLI supports two authentication methods, checked in this order:

  1. Environment variable: ARTIDROP_API_KEY=sk-... — best for CI/CD and agent automation
  2. Config file: ~/.config/artidrop/config.json — created by artidrop login
# Interactive login: opens browser for Google OAuth, stores token in config file
artidrop login

# Verify authentication
artidrop whoami
# > Authenticated as wen (wen@example.com)

# Logout (deletes config file)
artidrop logout

Config file format (~/.config/artidrop/config.json):

{
  "api_key": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "api_url": "https://api.artidrop.app"
}

The api_url field is for self-hosted users (Phase 3) and defaults to https://api.artidrop.app if omitted.

F3.3 Commands

artidrop publish <path-or-stdin>

The core command. Publishes a file and returns a URL.

# Publish a file
artidrop publish ./report.html
# > https://artidrop.app/a/x7k9m2

# Publish from stdin (must specify --format)
cat report.html | artidrop publish - --format html
# > https://artidrop.app/a/x7k9m2

# With options
artidrop publish ./report.html \
  --title "Q1 Revenue Report" \
  --visibility unlisted
# > https://artidrop.app/a/p3n8w1 (unlisted)

# Update an existing artifact (creates new version)
artidrop publish ./report-v2.html --update art_x7k9m2p4
# > https://artidrop.app/a/x7k9m2 (version 2)

Flags:

Flag Short Type Default Description
--title -t string filename without extension Artifact title
--format -f string inferred from extension html or markdown. Required when reading from stdin.
--visibility -v string public public or unlisted
--update -u string Existing artifact ID to update (creates new version)
--open -o boolean false Open the URL in the default browser after publishing
--json boolean false Output full JSON response instead of just the URL
--copy -c boolean false Copy the URL to the system clipboard

Format inference from file extension:

Output behavior:

Stdin detection:

artidrop list

Lists the authenticated user's artifacts.

artidrop list
# ID              TITLE                 FORMAT    VERSION  CREATED
# art_x7k9m2p4   Q1 Revenue Report     html      3        2026-03-22
# art_p3n8w1q2   API Documentation     markdown  1        2026-03-21
# art_k8j2m4n6   Dashboard Prototype   html      7        2026-03-20

artidrop list --json
# [{ "id": "art_x7k9m2p4", ... }, ...]

artidrop list --limit 5
Flag Short Type Default Description
--limit -l integer 20 Number of items
--offset integer 0 Pagination offset
--format -f string Filter by html or markdown
--json boolean false JSON output
artidrop get <artifact-id>

Shows details of a specific artifact.

artidrop get art_x7k9m2p4
# Title:      Q1 Revenue Report
# URL:        https://artidrop.app/a/x7k9m2
# Format:     html
# Version:    3
# Visibility: public
# Size:       2.1 KB
# Created:    2026-03-22T10:00:00Z
# Updated:    2026-03-23T14:00:00Z

artidrop get art_x7k9m2p4 --json
artidrop delete <artifact-id>

Deletes an artifact permanently.

artidrop delete art_x7k9m2p4
# Are you sure you want to delete "Q1 Revenue Report"? (y/N) y
# Deleted art_x7k9m2p4

# Skip confirmation
artidrop delete art_x7k9m2p4 --yes
Flag Short Type Default Description
--yes -y boolean false Skip confirmation prompt
artidrop versions <artifact-id>

Shows version history.

artidrop versions art_x7k9m2p4
# VERSION  SIZE     CREATED
# 3        3.0 KB   2026-03-23T14:00:00Z
# 2        2.5 KB   2026-03-22T18:00:00Z
# 1        2.0 KB   2026-03-22T10:00:00Z
artidrop login / artidrop logout / artidrop whoami

See F3.2 above.

F3.4 Exit codes

Code Meaning
0 Success
1 General error (network failure, server error)
2 Validation error (bad input, file not found, unsupported format)
3 Authentication error (not logged in, invalid API key)
4 Rate limit exceeded

F3.5 Acceptance criteria


F4. Artifact Rendering

F4.1 Artifact page layout

When a user visits artidrop.app/a/:shortId, they see the artifact page. This is the sharable destination.

Layout:

┌─────────────────────────────────────────────────┐
│  artidrop bar (slim, 40px height)               │
│  [logo]  "Q1 Revenue Report"    [Share] [Copy]  │
├─────────────────────────────────────────────────┤
│                                                 │
│                                                 │
│           Rendered artifact content             │
│           (full-width sandboxed iframe)         │
│                                                 │
│                                                 │
│                                                 │
└─────────────────────────────────────────────────┘

artidrop bar (top bar):

The bar is intentionally minimal so the artifact content is the hero.

Content area:

F4.2 Content serving and sandboxing

Artifact content is served from a separate origin to isolate user-generated content from the main application:

The artifact page at artidrop.app/a/:id embeds:

<iframe
  src="https://content.artidrop.app/:id"
  sandbox="allow-scripts allow-same-origin"
  allow="clipboard-write"
  style="width: 100%; height: calc(100vh - 40px); border: none;"
  loading="lazy"
></iframe>

Why separate origin: Even with sandbox, serving user HTML on the main domain risks cookie theft and session hijacking. The content subdomain (or separate domain) ensures that any malicious JavaScript in an artifact cannot access artidrop.app cookies, localStorage, or API tokens. See F7.1 for the two-domain architecture.

Content-Security-Policy on content domain:

Content-Security-Policy: default-src 'self' 'unsafe-inline' 'unsafe-eval'
  data: blob:;
  img-src *;
  media-src *;
  font-src *;
  style-src 'self' 'unsafe-inline' *;
  connect-src *;
  frame-src 'none';

This allows artifacts to load external images, fonts, and make API calls (many AI-generated artifacts fetch data), but prevents nested iframes (which could be used for clickjacking).

F4.3 HTML rendering

F4.4 Markdown rendering

Markdown artifacts are rendered to HTML at upload time. The API server converts Markdown to a complete HTML page and stores the rendered HTML alongside the original Markdown source. This keeps the content-serving layer simple (just serve static HTML) and makes artifact pages load fast.

Rendering pipeline (runs in the API server on publish):

  1. Parse Markdown using a CommonMark-compliant parser (remark or markdown-it)
  2. Support GitHub Flavored Markdown extensions: tables, task lists, strikethrough, autolinks, footnotes
  3. Syntax highlighting for fenced code blocks (via Shiki or Prism)
  4. Wrap in a styled HTML shell with a clean reading theme:
    • Max content width: 768px, centered
    • Font: system font stack (like GitHub)
    • Responsive images (max-width: 100%)
    • Anchor links on headings
  5. Include a table of contents sidebar if the document has 3+ headings (auto-generated from h1-h3)

Storage: Both the original Markdown source and the rendered HTML are stored. The rendered HTML is served to viewers. The raw Markdown is available via /a/:id/raw.

The artifact page (artidrop.app/a/:id) includes Open Graph and Twitter Card meta tags for rich previews:

<meta property="og:title" content="Q1 Revenue Report" />
<meta property="og:description" content="Published on artidrop" />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://artidrop.app/a/x7k9m2" />
<meta property="og:image" content="https://artidrop.app/a/x7k9m2/og-image" />
<meta property="og:site_name" content="artidrop" />

<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Q1 Revenue Report" />
<meta name="twitter:description" content="Published on artidrop" />
<meta name="twitter:image" content="https://artidrop.app/a/x7k9m2/og-image" />

OG image generation:

F4.6 Direct content URLs

URL Behavior
artidrop.app/a/:id Artifact page with chrome (top bar, share buttons, iframe)
artidrop.app/a/:id/v/:version Specific version of the artifact page
content.artidrop.app/:id Raw rendered content only (no chrome). HTML served directly (Markdown already rendered at upload time).
content.artidrop.app/:id/v/:version Specific version of raw rendered content
artidrop.app/a/:id/raw Source content as uploaded (HTML source or Markdown source as text/plain)

F4.7 Acceptance criteria


F5. Versioning

F5.1 Version model

F5.2 Storage

Each version stores:

The artifact record stores a current_version field pointing to the latest version number.

F5.3 Limits

Constraint Limit
Max versions per artifact (free) 20
Max versions per artifact (paid, Phase 2) Unlimited
Version content retention Permanent (deleted only when artifact is deleted)

When a free user exceeds 20 versions, the oldest version's content is deleted from storage (metadata retained so version numbers don't gap). The API returns a clear error: "Version limit reached. Upgrade to keep unlimited versions."

F5.4 Acceptance criteria


F6. Authentication and API Keys

F6.1 Authentication methods

Google OAuth:

  1. User clicks "Sign in with Google" on artidrop.app
  2. Redirected to Google OAuth consent screen
  3. artidrop requests scopes: openid, email, profile
  4. On callback, artidrop creates or updates the user record with Google ID, display name, email, avatar URL
  5. Session cookie set (artidrop_session, httpOnly, secure, sameSite=lax, 30-day expiry)

Google is the sole OAuth provider in Phase 1 because the product targets both non-technical and technical users. Nearly everyone has a Google account. GitHub OAuth is added in Phase 2 for developers who prefer it (see F8b).

API keys (for programmatic access):

F6.2 User dashboard

Authenticated users get a dashboard at artidrop.app/dashboard:

The dashboard is minimal in Phase 1 — no analytics, no teams, no billing.

F6.3 Data model

users
├── id              TEXT PRIMARY KEY  (e.g., "usr_abc123")
├── google_id       TEXT UNIQUE       (for Google OAuth)
├── email           TEXT UNIQUE
├── username        TEXT UNIQUE       (alphanumeric + hyphens, 3-39 chars)
├── display_name    TEXT
├── avatar_url      TEXT
├── created_at      TIMESTAMP
└── updated_at      TIMESTAMP

api_keys
├── id              TEXT PRIMARY KEY  (e.g., "key_abc123")
├── user_id         TEXT REFERENCES users(id)
├── name            TEXT              (user-assigned label)
├── key_hash        TEXT              (SHA-256 hash of the full key)
├── key_prefix      TEXT              (first 8 chars, for display: "sk-a1b2...")
├── created_at      TIMESTAMP
└── last_used_at    TIMESTAMP

artifacts
├── id              TEXT PRIMARY KEY  (e.g., "art_x7k9m2p4")
├── short_id        TEXT UNIQUE       (6-char base62 for URLs, e.g., "x7k9m2")
├── owner_id        TEXT REFERENCES users(id) NOT NULL
├── title           TEXT
├── format          TEXT              ("html" | "markdown")
├── visibility      TEXT              ("public" | "unlisted")
├── current_version INTEGER
├── size_bytes      INTEGER           (size of latest version)
├── created_at      TIMESTAMP
└── updated_at      TIMESTAMP

artifact_versions
├── id              TEXT PRIMARY KEY
├── artifact_id     TEXT REFERENCES artifacts(id)
├── version         INTEGER
├── content_hash    TEXT              (SHA-256 of content)
├── size_bytes      INTEGER
├── storage_key     TEXT              (object storage path)
├── created_at      TIMESTAMP
└── UNIQUE(artifact_id, version)

F6.4 Acceptance criteria


F7. Phase 1 Technical Architecture

F7.1 Component diagram (Phase 1 only)

┌──────────────┐  ┌──────────────┐
│  Web UI      │  │  CLI Tool    │
│  (React SPA) │  │  (npm pkg)   │
└──────┬───────┘  └──────┬───────┘
       │                 │
       └────────┬────────┘

┌──────────────────────────────────────────┐
│  Railway Service: api.artidrop.app       │
│  Node.js (Hono)                          │
│                                          │
│  /v1/artifacts    (CRUD API)             │
│  /v1/auth         (Google OAuth)         │
│  /v1/api-keys     (key management)       │
│  /*               (serves Web UI SPA)    │
├──────────────────────────────────────────┤
│  Also handles content.artidrop.app       │
│  (routes by Host header for sandboxing)  │
└──────┬───────────────────┬───────────────┘
       │                   │
┌──────▼───────┐   ┌───────▼──────────┐
│ PostgreSQL   │   │ Railway Buckets  │
│ (Railway)    │   │ (artifact        │
│ metadata     │   │  content files)  │
└──────────────┘   └──────────────────┘

Two domains, one Railway service:

Both domains point to the same Railway service. The server inspects the Host header and applies different response handling: main domain serves the app, content domain serves raw artifact HTML with strict CSP. Since content.artidrop.app is a subdomain, the session cookie must be set with domain=artidrop.app (exact, no leading dot) so it is NOT sent to the content subdomain. This ensures artifact JavaScript cannot access session cookies.

Alternative content domain: If subdomain cookie scoping proves tricky, use a completely separate domain (e.g., adrop-content.app) pointed at the same Railway service. This provides bulletproof origin isolation. Decide during implementation based on testing.

F7.2 Technology decisions (Phase 1)

Component Choice Rationale
API framework Hono Lightweight, fast, runs on Node.js. Portable to Cloudflare Workers or Deno Deploy if we need to migrate later.
Database PostgreSQL (Railway) Railway provides managed Postgres with zero setup. Reliable, familiar, handles all our query patterns.
Object storage Railway Buckets S3-compatible object storage built into Railway. No egress fees, no external service needed. Same platform as our API service and database, simplifying infrastructure management.
Web UI React + Vite SPA served as static files by the API service. No separate hosting needed.
CLI Node.js + Commander.js Ship to npm. Uses built-in fetch for HTTP.
Auth Google OAuth Arctic (lightweight OAuth library) for the Google flow + session cookies.
Markdown rendering remark + Shiki Runs at upload time in the API server. Stores rendered HTML in Railway Buckets alongside the Markdown source.
Deployment Railway Single service + managed Postgres. Simple deploy via railway up or GitHub push. Migrate to Fly.io or Cloudflare Workers when needed — Hono is portable.

Migration path: Hono runs unchanged on Cloudflare Workers, Deno Deploy, Bun, AWS Lambda, and Fly.io. PostgreSQL can be moved to any managed provider (Neon, Supabase, RDS). Railway Buckets is S3-compatible so any S3 backend works as a replacement. Nothing in Phase 1 creates platform lock-in.

F7.3 ID generation

F7.4 Storage layout in Railway Buckets

artidrop-content/
├── artifacts/
│   ├── art_x7k9m2p4/
│   │   ├── v1.html
│   │   ├── v2.html
│   │   └── v3.html
│   ├── art_p3n8w1q2/
│   │   ├── v1.md              (original Markdown source)
│   │   └── v1.rendered.html   (rendered HTML, served to viewers)
│   └── ...
└── og-images/
    ├── art_x7k9m2p4.png
    └── ...

For Markdown artifacts, two files are stored per version: the original .md source and the .rendered.html output. The content domain serves the .rendered.html file. The /a/:id/raw endpoint serves the .md source.

F7.5 Request flow: publish an artifact

Publishing:

1. Client → POST api.artidrop.app/v1/artifacts { content, format, title }
2. API validates input (auth, size, format, rate limit)
3. API generates artifact_id, short_id
4. If format is "markdown": render to HTML via remark + Shiki pipeline
5. API writes file(s) to Railway Buckets: artifacts/{artifact_id}/v1.html (or v1.md + v1.rendered.html)
6. API inserts artifact + artifact_version rows into PostgreSQL
7. API returns { id, url, version, ... }
8. Client displays URL to user

Viewing:

1. Viewer → GET artidrop.app/a/x7k9m2
2. Server detects Host=artidrop.app, serves the SPA shell
3. SPA calls GET api.artidrop.app/v1/artifacts/art_x7k9m2p4 (resolved from short_id)
4. SPA renders the artifact page with iframe src=content.artidrop.app/art_x7k9m2p4
5. Browser requests content.artidrop.app/art_x7k9m2p4
6. Server detects Host=content.artidrop.app, reads Railway Buckets key artifacts/art_x7k9m2p4/v3.html
7. Server returns HTML with CSP headers (no session cookie sent — different origin)
8. Browser renders artifact in sandboxed iframe

OG tag serving (crawlers): When the server detects a crawler User-Agent (facebookexternalhit, Twitterbot, Slackbot, Discordbot, etc.) requesting artidrop.app/a/:id, it returns a minimal HTML page with only OG meta tags — not the full SPA. This ensures rich link previews work without JavaScript.

F7.6 CLI login flow

The artidrop login command uses a local HTTP callback server (same pattern as gh auth login):

1. CLI starts a temporary local HTTP server on a random available port (e.g., localhost:9876)
2. CLI opens the browser to: artidrop.app/cli-auth?port=9876
3. User signs in with Google on artidrop.app (if not already signed in)
4. artidrop.app generates a one-time API key for the CLI session
5. artidrop.app redirects browser to: localhost:9876/callback?key=sk-xxxxx
6. Local server receives the key, saves it to ~/.config/artidrop/config.json
7. Local server responds with a "Success! You can close this tab." HTML page
8. CLI prints "Authenticated as {username}" and exits

If the browser cannot be opened (headless/SSH environment), the CLI falls back to a manual flow:

1. CLI prints: "Open this URL in your browser: artidrop.app/cli-auth?manual=true"
2. User opens URL, signs in, sees a one-time code
3. User pastes the code into the CLI prompt
4. CLI exchanges the code for an API key via the API

6. Phase 2 and Phase 3 Features (Summary)

Phase 2: Growth

Features that drive adoption and retention after MVP.

F8. MCP Server

F8b. GitHub OAuth and Account Linking

F9. SDKs (Python and TypeScript)

Python:

from artidrop import Artidrop

client = Artidrop(api_key="sk-...")

# Publish HTML string
result = client.publish("<h1>Hello</h1>", format="html", title="Greeting")

# Publish from file
result = client.publish_file("./report.html", title="Q1 Report")

# Update existing
result = client.update("art_x7k9m2p4", "<h1>Updated</h1>")

# List artifacts
artifacts = client.list(limit=10)

# Delete
client.delete("art_x7k9m2p4")

TypeScript:

import { Artidrop } from 'artidrop';

const client = new Artidrop({ apiKey: 'sk-...' });

const result = await client.publish({
  content: '<h1>Hello</h1>',
  format: 'html',
  title: 'Greeting',
});

console.log(result.url);

F10. Embeds

F11. Analytics

F12. Multi-File Artifacts

F13. Custom Slugs and Vanity URLs

Phase 3: Scale

Features for teams, enterprises, and long-term sustainability.

F14. Workspaces and Teams

F15. Custom Domains

F16. Access Control

F17. Artifact Collections

F18. Self-Hosted Edition

7. Metrics and Success Criteria

MVP launch (Phase 1)

Metric Target Timeframe
Artifacts published 1,000 First 30 days
Unique publishers 200 First 30 days
CLI installs (npm) 500 First 30 days
API-published artifacts (% of total) >30% First 30 days

Growth (Phase 2)

Metric Target Timeframe
Monthly active publishers 2,000 6 months post-launch
Artifacts published per month 20,000 6 months post-launch
MCP server installations 500 6 months post-launch
SDK downloads (npm + PyPI combined) 5,000/month 6 months post-launch

Scale (Phase 3)

Metric Target Timeframe
Paying workspaces 100 12 months post-launch
Monthly recurring revenue $5,000 12 months post-launch
Self-hosted instances (Docker pulls) 1,000 12 months post-launch

8. Pricing Model

Tier Price Includes
Free $0 50 artifacts, 1GB storage, 10GB bandwidth/month, artidrop.app URLs, basic API access (requires sign-in)
Pro $9/month Unlimited artifacts, 10GB storage, 100GB bandwidth/month, custom slugs, analytics, versioning history (unlimited), priority support
Team $29/month Everything in Pro + workspaces (up to 10 members), workspace API keys, custom domains, access control, 50GB storage, 500GB bandwidth/month
Self-hosted Free (open-source) Core API + CLI + rendering. No analytics, no custom domains, no team management. Community support only.

9. Risks and Mitigations

Risk Impact Mitigation
Abuse (hosting malware, phishing) Reputational damage, domain blacklisting Content scanning on upload, abuse reporting, rate limiting, terms of service, domain reputation monitoring
Low adoption Product fails Focus on MCP distribution (agents discover artidrop), integrate with popular agent frameworks, generous free tier with Google sign-in (one click)
Infrastructure costs outpace revenue Financial loss Railway starter plan ($5/mo) + Railway Buckets (no egress fees, included in Railway billing), aggressive caching, file size limits, bandwidth caps on free tier
Competitor launches same product with VC funding Market share loss Open-source core creates moat (self-hosters become community), ship fast, focus on developer experience
Platform dependency (Claude/ChatGPT change artifact handling) Reduced demand Stay LLM-agnostic, position as universal publishing layer, not tied to any one AI platform

10. Open Questions

  1. Should the CLI auto-open the published URL in the browser? (Convenient for humans, noise for agents — currently opt-in via --open flag)
  2. Should we build a "remix/fork" feature (like Claude Artifacts)? (Community feature vs. scope creep)
  3. Should free-tier artifacts have a maximum lifetime? (Cost control vs. user expectations — currently set to permanent)
  4. Content domain: subdomain or separate domain? (content.artidrop.app vs a separate domain like adrop-content.app for bulletproof cookie isolation — decide during implementation)

11. Phasing and Timeline

Phase 1 breakdown

Week Milestone Deliverables
1 Project setup + API core Repo, CI, Railway service + Postgres, Railway Bucket, domain setup (artidrop.app + content domain). API: POST /v1/artifacts, GET /v1/artifacts/:id. Content serving from Railway Buckets via Host-based routing.
2 API complete + rendering Remaining CRUD endpoints, versioning, Markdown rendering pipeline (remark + Shiki at upload time), content sandboxing on separate origin, OG meta tags for crawlers.
3 Authentication Google OAuth flow, API key create/revoke, session management, auth middleware for all write endpoints.
4 CLI tool publish, list, get, delete, versions, login/logout/whoami commands. Stdin support, --json output, proper exit codes. Publish to npm.
5 Web UI Landing page with drag-and-drop + paste, success state with URL/QR, sign-in/sign-up pages, user dashboard (artifact list, API key management).
6 Polish + launch End-to-end testing, edge cases (rate limiting, error pages for 404/deleted), landing page copy, docs site, public launch.

Overall timeline

Phase Scope Duration
Phase 1: MVP Web UI, REST API, CLI, rendering, versioning, auth/API keys 6 weeks
Phase 2: Growth MCP server, Python/TS SDKs, embeds, analytics, vanity URLs 4-6 weeks after MVP
Phase 3: Scale Workspaces/teams, custom domains, access control, collections, self-hosted edition 8-12 weeks after Phase 2