# 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 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 ```python from artidrop import Artidrop client = Artidrop(api_key="sk-...") result = client.publish( content="...

Q1 Revenue Report

...", 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 `` or `

Hello World

", "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):** ```json { "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 ` {title} {content} ``` ##### 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):** ```json { "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:** - `public` artifacts: anyone can read metadata - `unlisted` artifacts: 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):** ```json { "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:** ```json { "content": "

Updated Report

", "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):** ```json { "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: ```json { "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 ```bash 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` ```bash # 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`): ```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 ` The core command. Publishes a file and returns a URL. ```bash # 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, `--format` is required ##### `artidrop list` Lists the authenticated user's artifacts. ```bash 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 ` Shows details of a specific artifact. ```bash 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 ` Deletes an artifact permanently. ```bash 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 ` Shows version history. ```bash 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.html` prints a working URL to stdout and exits 0 - [ ] Piping from stdin works: `echo '

Hi

' | artidrop publish - --format html` - [ ] `--json` flag outputs valid JSON to stdout - [ ] Errors go to stderr, never stdout (scripts can safely capture `$(artidrop publish ...)`) - [ ] `--update` creates a new version and returns the same base URL - [ ] `artidrop list` shows a formatted table with ID, title, format, version, and date - [ ] `artidrop delete` prompts for confirmation; `--yes` skips it - [ ] Unauthenticated `publish` command fails with a clear "Please sign in first" message and exit code 3 - [ ] `ARTIDROP_API_KEY` env 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 ` ``` **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):** 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`. #### 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: ```html ``` **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.app` cookies 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/:id` with `content`) 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 artifact - `version`: 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 size - `storage_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 `--update` increments version number by 1 - [ ] `/a/:id` serves the latest version content - [ ] `/a/:id/v/1` serves 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:** 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):** - 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 callbacks - `content.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_`): prefix `art_` + 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_`): prefix `usr_` + 8 random alphanumeric chars. - **API key ID** (`key_`): prefix `key_` + 8 random alphanumeric chars. - **API key value**: `sk-` + 32 random hex chars. Generated via `crypto.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 URL - `artidrop_update` -- update an existing artifact - `artidrop_list` -- list user's artifacts - `artidrop_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_id` column (nullable) to the `users` table - Users can then sign in with either provider #### F9. SDKs (Python and TypeScript) Python: ```python from artidrop import Artidrop client = Artidrop(api_key="sk-...") # Publish HTML string result = client.publish("

Hello

", format="html", title="Greeting") # Publish from file result = client.publish_file("./report.html", title="Q1 Report") # Update existing result = client.update("art_x7k9m2p4", "

Updated

") # List artifacts artifacts = client.list(limit=10) # Delete client.delete("art_x7k9m2p4") ``` TypeScript: ```typescript import { Artidrop } from 'artidrop'; const client = new Artidrop({ apiKey: 'sk-...' }); const result = await client.publish({ content: '

Hello

', 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: `