{"slug": "how-we-built-a-skills-marketplace-for-claude-and-how-you-can-too", "title": "How we built a skills marketplace for Claude (and how you can too)", "summary": "Photoroom co-founder Eliot built an internal skills marketplace that stores Claude skills as rows in a single Notion database, with a Cloudflare Worker syncing a search index and answering plain-language queries. The Worker embeds only three intent fields — title, description and \"when to use this skill\" — using Cloudflare Workers AI, stores one vector per skill in Vectorize with metadata in D1, and fetches the full skill body live from Notion on selection; a daily cron job re-reconciles the index in case a webhook is dropped. The setup replaces the two existing sharing options — pushing skills into a shared space as its owner or having each person add skills manually — which Photoroom says left skills scattered, duplicated or lost across engineers, product managers, marketers, designers and ops.", "body_md": "## How we share skills across Photoroom\n\nSharing skills inside a company is harder than it sounds.\n\nIn the data team, we built a skill that helps people create their Amplitude events and properties directly from Claude. It carries all the context on our naming conventions and rules, and makes sure people follow them by the book. We wanted to share it widely, well beyond the developers who first needed it. And it's not just us. Skills are popping up everywhere at Photoroom. Engineers write them, but so do product managers, marketers, designers, ops... Anyone who figures out a good way to get something done with Claude ends up capturing it as a skill. That's great, except every one of those skills is stuck wherever its author left it, useful to one person instead of the whole company.\n\nToday there are only two ways to do it: either you own a shared space and are owner of the space and push skills into it, or every single person has to add the skill manually on their own side. Neither scales. There's no clean way for one person to write a skill and have everyone else discover and reuse it, so skills end up scattered, duplicated, or simply lost.\n\nEliot, our co-founder, built something to fix this: a skills marketplace. The idea is simple. Everyone adds their skills to one Notion database, and a search engine on top lets anyone find the right skill for whatever they're trying to do, just by describing it in plain language. No git commit, no friction, just a Notion database. And I think it is brilliant! I am adding the technical steps for you to implement it at the end of the article.\n\n## See it in action\n\nI ask Claude to create events and properties. That’s it. No additional context, not even the name of Amplitude. Claude reads the `Photoroom skill marketplace` right away and **infers** that according to my request, it will read the Amplitude event creation skill.\n\nThen the skill behaves as expected and ensures I follow our strict naming convention policy, our mandatory description rule, past tense convention etc.\n\n# How does it work?\n\n## The Notion database is the source of truth\n\nEvery skill lives as a row in a single Notion database. Each row has a few intent fields: a title, a skill description, a \"when to use this skill\" field, the teams it's relevant to, and an owner. The actual skill instructions live in the body of the page.\n\nThat's the only thing people have to maintain. You add your skill to Notion like any other page, and the rest is automatic.\n\n## A Cloudflare Worker does the heavy lifting\n\nSitting between Notion and the people searching is a single Cloudflare Worker. It has two jobs:\n\nThe first is keeping a search index in sync with Notion. Every time a skill page changes, Notion fires a webhook to the Worker. The Worker takes only the three intent fields (title, description, and \"when to use\"), turns them into a vector embedding using “Workers AI”, and stores one vector per skill in “Vectorize”, with the metadata in “D1”. Workers AI, Vectorize, and D1 are all Cloudflare products running inside the same Worker. A daily cron job re-reconciles everything as a safety net, in case a webhook ever gets dropped.\n\nThe second job is answering searches. When someone types a question, the Worker embeds the question, finds the closest skills in Vectorize, and returns a ranked list. When you pick one, it fetches the full skill body live from Notion so you always get the current version.\n\n## Why only the intent fields get embedded\n\nThis is the clever part. The Worker deliberately does not embed the skill body. Search ranks on what a skill is *for*, not on random keywords buried in its implementation. So a search like \"I want to create these events\" matches the skill that describes that intent, rather than every skill that happens to mention the word \"event\" somewhere in its code.\n\nThe body is never indexed, only fetched on demand. That keeps results clean and the content always fresh.\n\n## The one skill that unlocks the rest\n\nThere's one thing to set up first: the marketplace lets us share skills, but Claude needs to know the marketplace exists before it can search it. We solved that with a single bootstrap skill.\n\nEliot, who is space owner of our Claude Team, added the single skill `Photoroom skill marketplace` stated above to our shared Claude Team. That skill is what knows how to reach the marketplace, and it was installed once, for the whole company. Nobody else has to install anything. The moment you add a skill to the Notion database, it's reachable by everyone straight away. One skill to find all the others.\n\n**Finding a skill** mostly happens from Claude, and you don't do anything special. Whenever you ask Claude how something is done internally at Photoroom (a workflow, a tool, a policy, who to contact, a past playbook), it searches the marketplace on its own, finds the most relevant skill, and pulls back its full content to answer you. You just ask in plain language. Under the hood it's the two-step read path: Claude searches by intent, gets a ranked list with a similarity score, then fetches the full body of the best match live from Notion. If nothing scores high enough, it won't force a match, so you won't get an irrelevant skill dressed up as an answer. If you'd rather browse by hand, the search UI is also embeddable directly in Notion.\n\n**Adding a skill** is just adding a row to the Notion database. Fill in the title, description, and \"when to use\" fields properly, because those three are what search runs on. Drop the actual instructions in the body of the page. The index updates itself within seconds via the webhook, and your skill is instantly discoverable by everyone.\n\n## What this changes\n\nThe quiet but important part is that Claude doesn't only reach for the marketplace when you explicitly ask for a skill. It also checks it whenever it's about to give you Photoroom-specific advice it can't verify on its own. So if someone asks how we name Amplitude events, how a hiring loop runs, or which vendor we use for something, Claude looks for a captured skill before answering, instead of guessing. You are now basically using skills without invoking them and without knowing it even exists.\n\nThat's the shift. Claude stops being a generic assistant and starts answering with our actual internal know-how, written by the person who already hit the gotchas in that domain. And it works the other way too: the moment you capture something you know in a skill, everyone's Claude knows it. This is a very simple but powerful way to share instructions across the company, without any Git commit.\n\n## Known limitations\n\nThere's no auth on the search endpoint yet in v1, so it's fronted with Cloudflare Access (our Google Workspace) before being shared more widely. And the embedding model is English-tuned for now. If we start writing skills in French, we'll swap in a multilingual model.\n\n## Takeway\n\nThat's the whole idea: one Notion database as the source of truth, one Cloudflare Worker to index and search it, and a single skill so Claude knows the marketplace exists. No git commits, no per-person setup, no scattered skills. You write something down once, and everyone's Claude can find it.\n\nIf your company uses Claude, you probably have the same problem we did: real know-how stuck in people's heads or buried in docs, with no clean way to put it where Claude will actually reach for it. This is one way to fix that, and the best part is how little it takes to reproduce. The rest of this article is the exact recipe, so you can build your own in an afternoon.\n\n# Technical steps\n\n## One-time setup\n\n```\n# 0. Install deps\nnpm install\n\n# 1. Create the D1 database. Copy the printed `database_id` into wrangler.toml.\nwrangler d1 create skill_marketplace\n\n# 2. Apply migrations (remote = real CF account; local = dev sandbox)\nnpm run migrate:remote\nnpm run migrate:local\n\n# 3. Create the Vectorize index (768 dims, cosine — matches bge-base-en-v1.5)\nwrangler vectorize create skill-marketplace \\\\\n  --dimensions=768 --metric=cosine\n\n# 4. Add metadata indexes so the query handler can return useful metadata\nwrangler vectorize create-metadata-index skill-marketplace \\\\\n  --property-name=page_id --type=string\nwrangler vectorize create-metadata-index skill-marketplace \\\\\n  --property-name=title --type=string\nwrangler vectorize create-metadata-index skill-marketplace \\\\\n  --property-name=teams --type=string\nwrangler vectorize create-metadata-index skill-marketplace \\\\\n  --property-name=url --type=string\n\n# 5. Set secrets\nwrangler secret put NOTION_TOKEN\n# (Workspace integration token; needs Read content capability)\n\n# 6. Deploy (we need a public URL before we can register the webhook)\nnpm run deploy\n```\n\nAfter step 6 you'll have a URL like `https://skill-marketplace.<account>.workers.dev`.\n\n## Register the Notion webhook\n\n1. Go to [https://www.notion.so/profile/integrations](https://www.notion.so/profile/integrations) and open the**Skill Marketplace** integration.\n2. **Webhooks** tab →**Create a subscription** .\n3. **URL** :`https://skill-marketplace.<account>.workers.dev/webhook`\n4. **Events** : check\n  - `page.created`\n  - `page.properties_updated`\n  - `page.content_updated`\n  - `page.deleted`\n  - `page.undeleted` (optional, for restores)\n5. Click **Subscribe** . Notion POSTs`{ verification_token }` to the URL.\n6. Stream Worker logs to grab it: \n Look for:`[webhook] verification_token received — paste into Notion UI ...`\n\n```\nnpm run tail\n```\n\n7. Paste the token back into the Notion UI **and** store it as the Worker\n secret so signatures can be verified:\n\n```\nwrangler secret put NOTION_WEBHOOK_SECRET\n```\n\n8. Click **Verify** in Notion. The subscription becomes active.\n\nBackfill the index once (webhooks only cover *future* changes):\n\n```\ncurl -X POST \"https://skill-marketplace.<account>.workers.dev/admin/reconcile\" \\\\\n  -H \"X-Admin-Token: $NOTION_WEBHOOK_SECRET\"\n```\n\n## Endpoints\n\n| Method | Path | What it does | \n|---|---|---|\n| GET | `/` | The search UI, embeddable in Notion | \n| POST | `/api/search` | Step 1: send a question, get ranked skills back | \n| GET | `/api/skills/<skill_id>` | Step 2: fetch one skill's full content | \n| POST | `/webhook` | Receives Notion change events | \n| POST | `/admin/sync/<page_id>` | Re-sync a single page (admin only) | \n| POST | `/admin/reconcile` | Re-sync everything (admin only) | \n\nCron (`0 5 * * *` UTC) also runs `reconcileAll` daily as a safety net for any\n\ndropped webhook events.\n\n## Local dev\n\n```\n# Use a real Notion token but the local D1/Vectorize sandbox.\necho 'NOTION_TOKEN = \"ntn_...\"' > .dev.vars\necho 'NOTION_WEBHOOK_SECRET = \"dummy-for-local\"' >> .dev.vars\nnpm run dev\n\n# In another shell, re-sync one page into the local index:\ncurl -X POST \"<http://localhost:8787/admin/sync/><page_id>\" \\\n  -H \"X-Admin-Token: dummy-for-local\"\n\n# Then search against it:\ncurl -X POST \"<http://localhost:8787/api/search>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"q\":\"how do I create an amplitude branch?\"}'\n```\n\nVectorize is not available in `wrangler dev` local mode by default — pass`--remote` to run against the live index while iterating.\n\n### Calling the API directly\n\nThe marketplace is a small HTTP API in front of the Notion database. Two steps: search by intent, then fetch the full skill.\n\n**Search** returns the top matches for a natural-language query:\n\n```\ncurl -sS -X POST \"<https://skill-marketplace.xxx.workers.dev/api/search>\" \\\n  -H \"X-API-Key: <api-key>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"q\":\"<user intent in natural language>\",\"top_k\":5}'\n```\n\nEach result carries a `score` (cosine similarity, 0 to 1). Below ~0.5 usually means nothing relevant exists, so don't force a match.\n\n**Fetch** returns the full skill body in the `content` field for a given `skill_id`\n\n```\ncurl -sS \"<https://skill-marketplace.xxx.workers.dev/api/skills/><skill_id>\" \\\n  -H \"X-API-Key: <api-key>\"\n```\n\n", "url": "https://wpnews.pro/news/how-we-built-a-skills-marketplace-for-claude-and-how-you-can-too", "canonical_source": "https://www.photoroom.com/inside-photoroom/how-we-built-a-skills-marketplace-for-claude", "published_at": "2026-11-09 12:52:36.094014+00:00", "updated_at": "2026-11-09 12:52:38.649077+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "ai-infrastructure", "ai-products"], "entities": ["Photoroom", "Claude", "Eliot", "Notion", "Cloudflare", "Workers AI", "Vectorize", "D1"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/how-we-built-a-skills-marketplace-for-claude-and-how-you-can-too", "markdown": "https://wpnews.pro/news/how-we-built-a-skills-marketplace-for-claude-and-how-you-can-too.md", "text": "https://wpnews.pro/news/how-we-built-a-skills-marketplace-for-claude-and-how-you-can-too.txt", "jsonld": "https://wpnews.pro/news/how-we-built-a-skills-marketplace-for-claude-and-how-you-can-too.jsonld"}}