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:
- Manually copy-paste outputs into a hosting platform (Netlify, Vercel) that requires account setup and project configuration
- Use ephemeral pastebins that expire or don't render HTML
- Rely on platform-specific sharing (Claude Artifacts) that has no programmatic API and only works within that platform's UI
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
- Building with the Anthropic API, OpenAI API, LangChain, CrewAI, Google ADK, or custom frameworks
- Need their agents to programmatically publish outputs (reports, dashboards, HTML apps) without human intervention
- Value simplicity: one function call or CLI command, no project setup
Secondary: AI power users (non-developers)
- Use Claude, ChatGPT, or other AI tools daily
- Generate HTML artifacts, Markdown documents, or interactive visualizations
- Want to share outputs with colleagues, clients, or publicly -- without knowing how to deploy a website
- Currently copy-pasting into Google Docs, taking screenshots, or using Claude's built-in publish (limited)
Tertiary: Teams and organizations
- Multiple agents producing artifacts across projects
- Need centralized management, access control, and analytics
- Want custom domains and branding for published content
4. User Journeys
Journey 1: Consumer drag-and-drop
- User generates an HTML artifact in Claude / ChatGPT / any AI tool
- User saves the file locally (or copies the HTML)
- User opens artidrop.app, signs in with Google (one click)
- User drags the file onto the page (or pastes HTML)
- artidrop returns a live URL (e.g.,
artidrop.app/a/x7k9m2) - User shares the URL -- recipient sees the rendered artifact immediately
Journey 2: Developer CLI publish
- Developer installs:
npm install -g artidrop(orpip install artidrop) - Agent generates an HTML file at
./output/report.html - Agent runs:
artidrop publish ./output/report.html - CLI prints:
Published: https://artidrop.app/a/x7k9m2 - Agent includes the URL in its response to the user, sends it via Slack, emails it, etc.
- First use requires
artidrop loginor settingARTIDROP_API_KEYenv 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)
- User configures artidrop MCP server in Claude Code, Cursor, or another MCP-aware client
- During a conversation, the agent generates an artifact and decides to publish it
- Agent calls the
artidrop_publishMCP tool with the HTML content - Tool returns the live URL
- Agent presents the URL to the user
Journey 5: Team dashboard
- Team admin creates an artidrop workspace, invites team members
- Multiple agents across the team publish artifacts using workspace API keys
- All artifacts appear in the team dashboard -- searchable, filterable by agent/date/tag
- Admin sets a custom domain (
reports.company.com) for published artifacts - 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):
- 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.
- 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").
- 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.
- 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.
- 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):
- ZIP archives (Phase 2: multi-file artifacts)
- Images, PDFs, or other binary files
- URLs to fetch and host
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
- Signed-out users see the hero section with "Sign in with Google" CTA
- Signed-in users see the drop zone and can drag-and-drop an HTML file and receive a URL within 3 seconds
- User can paste HTML into the editor and publish with one click
- User can switch to Markdown tab, paste Markdown, and publish
- Published URL renders the artifact correctly in a new browser tab
- Copy button copies the URL to the clipboard and shows a "Copied!" confirmation
- File validation errors display inline without a page reload
- Rate-limited users see a clear message with retry timing
- The page works on mobile (responsive drop zone, paste mode works on small screens)
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:
GET /v1/artifacts/:idallows unauthenticated requests for public/unlisted artifacts- Artifact content serving (
GET /a/:id) is unauthenticated (public access)
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:
publicartifacts: anyone can read metadataunlistedartifacts: anyone with the ID can read metadata (not listed in search/browse)- Artifacts owned by the authenticated user: always accessible
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:
- Environment variable:
ARTIDROP_API_KEY=sk-...— best for CI/CD and agent automation - Config file:
~/.config/artidrop/config.json— created byartidrop 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:
.html,.htm→html.md,.markdown→markdown- No extension or unrecognized → error: "Cannot infer format. Use --format html or --format markdown."
Output behavior:
- Default: prints only the URL to stdout (so it can be captured by scripts:
URL=$(artidrop publish ./f.html)) --json: prints the full API response as pretty-printed JSON--copy: prints the URL AND copies to clipboard- Errors and progress messages go to stderr (never pollute stdout)
Stdin detection:
- If the argument is
-or if stdin is not a TTY (pipe detected), read from stdin - When reading from stdin,
--formatis required
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
-
artidrop publish ./file.htmlprints a working URL to stdout and exits 0 - Piping from stdin works:
echo '<h1>Hi</h1>' | artidrop publish - --format html -
--jsonflag outputs valid JSON to stdout - Errors go to stderr, never stdout (scripts can safely capture
$(artidrop publish ...)) -
--updatecreates a new version and returns the same base URL -
artidrop listshows a formatted table with ID, title, format, version, and date -
artidrop deleteprompts for confirmation;--yesskips it - Unauthenticated
publishcommand fails with a clear "Please sign in first" message and exit code 3 -
ARTIDROP_API_KEYenv var takes precedence over config file - Readable error messages for common failures (no auth, rate limited, file not found, network error)
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):
- artidrop logo (links to artidrop.app)
- Artifact title
- "Share" button: copies URL to clipboard
- Version indicator if version > 1: "v3" with dropdown to view other versions
- "Raw" link: opens
/a/:id/raw(the source HTML/Markdown) - If the viewer is the owner: "Edit" link (opens web editor, Phase 2), "Delete" button
The bar is intentionally minimal so the artifact content is the hero.
Content area:
- Full-width, full remaining viewport height
- Rendered inside an
<iframe>with itssrcpointing to the content domain
F4.2 Content serving and sandboxing
Artifact content is served from a separate origin to isolate user-generated content from the main application:
- Main app:
artidrop.app - Content serving:
content.artidrop.app
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
- Content served exactly as stored, with
Content-Type: text/html; charset=utf-8 - If the stored content was auto-wrapped (see F2.2), it is served in its wrapped form
- No post-processing, minification, or transformation — what you upload is what gets served
- External resources (CDN scripts, stylesheets, images) are allowed — artifacts commonly reference libraries like Chart.js, D3, Tailwind CDN, etc.
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):
- Parse Markdown using a CommonMark-compliant parser (remark or markdown-it)
- Support GitHub Flavored Markdown extensions: tables, task lists, strikethrough, autolinks, footnotes
- Syntax highlighting for fenced code blocks (via Shiki or Prism)
- 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
- 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.
F4.5 Meta tags and link previews
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:
- Auto-generated image (1200x630) with the artifact title, artidrop logo, and a light brand pattern
- Generated on first request and cached
- Uses a simple template (not a screenshot of the artifact — that's expensive and deferred)
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
- HTML artifact renders identically to opening the same file locally in a browser
- Markdown artifact renders with syntax-highlighted code blocks, tables, and task lists
- Artifacts using external CDN resources (Tailwind, Chart.js, D3) load correctly
- Sharing an artifact URL on Slack/Discord/Twitter shows a rich link preview with title
- Artifact page loads in under 2 seconds globally (edge-cached)
- JavaScript in an artifact cannot access
artidrop.appcookies or localStorage - Navigating to a deleted artifact shows a clean 404 page ("This artifact has been deleted")
- Version URLs (
/a/:id/v/1) serve the correct historical content
F5. Versioning
F5.1 Version model
- Every artifact has an ordered sequence of immutable versions, numbered starting from 1
- The artifact's canonical URL (
/a/:id) always resolves to the latest version - Previous versions are accessible via
/a/:id/v/:number - Updating an artifact (via
PUT /v1/artifacts/:idwithcontent) creates a new version - Metadata-only updates (title, visibility) do not create a new version
- Versions cannot be individually deleted — deleting an artifact removes all versions
F5.2 Storage
Each version stores:
artifact_id: parent artifactversion: integer (1, 2, 3, ...)content_hash: SHA-256 hash of the content (for deduplication detection — not deduped in Phase 1, but hash stored for future use)size_bytes: content sizestorage_key: key in object storage (e.g.,artifacts/{artifact_id}/v{version}.html)created_at: timestamp
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
- Publishing with
--updateincrements version number by 1 -
/a/:idserves the latest version content -
/a/:id/v/1serves the original content even after 5 updates - Version list API returns all versions in reverse chronological order
- Metadata-only updates do not create a version
- Duplicate content (same hash) still creates a new version (we don't skip duplicates — the user explicitly asked to update)
F6. Authentication and API Keys
F6.1 Authentication methods
Google OAuth:
- User clicks "Sign in with Google" on artidrop.app
- Redirected to Google OAuth consent screen
- artidrop requests scopes:
openid,email,profile - On callback, artidrop creates or updates the user record with Google ID, display name, email, avatar URL
- 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):
- Users create API keys in the dashboard or via CLI
- Key format:
sk-prefix + 32 random hex characters (e.g.,sk-a1b2c3d4e5f6...) - Keys are hashed (SHA-256) before storage — the full key is shown only once at creation time
- Each key has a name (user-assigned label, e.g., "my-agent", "ci-pipeline")
- Keys can be revoked individually
- Limit: 10 API keys per user (free tier)
F6.2 User dashboard
Authenticated users get a dashboard at artidrop.app/dashboard:
- Artifacts list: table of all artifacts with columns: title, format, visibility, version, size, created. Click to view. Actions: open, copy URL, delete.
- API keys: create, list (name + last-4-chars + created date), revoke.
- Account settings: username, email, connected Google account, delete account.
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
- User can sign in with Google and see their dashboard
- User can create an API key, see it once, and use it for CLI/API auth
- User can revoke an API key and it immediately stops working
- API key auth and session cookie auth both work for all authenticated endpoints
- All write endpoints return 401 for unauthenticated requests
- Visiting a deleted artifact shows a clean 404 page
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:
artidrop.app— serves the SPA, API, OAuth callbackscontent.artidrop.app— serves artifact HTML content with sandboxing headers
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
- Artifact ID (
art_): prefixart_+ 8 random alphanumeric chars (base62). Example:art_x7k9m2p4. Collision probability negligible at MVP scale (~218 trillion possible IDs). - Short ID (for URLs): 6 random base62 chars. Example:
x7k9m2. Used in the public URL path (/a/x7k9m2). On collision, regenerate (check DB). - User ID (
usr_): prefixusr_+ 8 random alphanumeric chars. - API key ID (
key_): prefixkey_+ 8 random alphanumeric chars. - API key value:
sk-+ 32 random hex chars. Generated viacrypto.randomBytes(16).toString('hex').
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
- Publish as an MCP tool server that any MCP-aware client (Claude Code, Cursor, Windsurf, etc.) can connect to
- Tools exposed:
artidrop_publish-- publish content, returns URLartidrop_update-- update an existing artifactartidrop_list-- list user's artifactsartidrop_delete-- delete an artifact
- Configuration: user provides API key via MCP server config
F8b. GitHub OAuth and Account Linking
- Add "Sign in with GitHub" as a secondary auth option (scopes:
read:user,user:email) - Account linking: if a user signs in with GitHub using the same email as an existing Google account, the accounts are automatically merged
- Add
github_idcolumn (nullable) to theuserstable - Users can then sign in with either provider
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
- oEmbed endpoint (
/oembed?url=...) for automatic rich embeds in Notion, Slack, Discord, etc. - Embed snippet:
<iframe src="artidrop.app/a/:id/embed" ...>for embedding in blogs, docs, READMEs - Compact embed mode (no chrome, just content) and full mode (with artidrop header bar)
F11. Analytics
- Per-artifact: page views, unique visitors, referrers, geographic distribution
- Dashboard view: aggregate stats across all artifacts
- API endpoint:
GET /v1/artifacts/:id/analytics
F12. Multi-File Artifacts
- Accept ZIP archives or directories containing an
index.htmland supporting files (CSS, JS, images) - API accepts multipart form data for file upload
- Storage: each version stored as a directory in Railway Buckets (
artifacts/{id}/v{version}/index.html, etc.) - Content serving resolves relative paths within the artifact directory
F13. Custom Slugs and Vanity URLs
- User-chosen slugs:
artidrop.app/a/my-cool-report(unique per user/workspace) - Namespaced URLs:
artidrop.app/@username/my-cool-report
Phase 3: Scale
Features for teams, enterprises, and long-term sustainability.
F14. Workspaces and Teams
- Create workspaces, invite members by email
- Roles: owner, admin, member, viewer
- Workspace-scoped API keys
- All artifacts published with workspace API keys visible in the workspace dashboard
F15. Custom Domains
- Map a custom domain (e.g.,
reports.company.com) to a workspace - Automatic SSL via Let's Encrypt
- Artifacts accessible at
reports.company.com/q1-report
F16. Access Control
- Visibility levels: public, unlisted (anyone with link), private (authenticated users only)
- Password-protected artifacts
- Workspace-scoped private artifacts (visible only to workspace members)
F17. Artifact Collections
- Group related artifacts into named collections
- Collection URL:
artidrop.app/@username/collections/q1-reports - Collections have their own landing page with table of contents
F18. Self-Hosted Edition
- Open-source core: API server, CLI, rendering engine
- Docker image and docker-compose for easy self-hosting
- Supports local filesystem, S3, or Railway Buckets as storage backends
- SQLite for metadata (single-binary deployment)
- Cloud edition adds: analytics, custom domains, team management, CDN
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
- Should the CLI auto-open the published URL in the browser? (Convenient for humans, noise for agents — currently opt-in via
--openflag) - Should we build a "remix/fork" feature (like Claude Artifacts)? (Community feature vs. scope creep)
- Should free-tier artifacts have a maximum lifetime? (Cost control vs. user expectations — currently set to permanent)
- Content domain: subdomain or separate domain? (
content.artidrop.appvs a separate domain likeadrop-content.appfor 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 |
Appendix: Related Research
- Market Landscape -- survey of 30+ existing products
- Competitive Analysis -- feature matrix and positioning
- Market Gaps -- identified opportunities
- Technical References -- open-source projects and infrastructure options