Form builder plugin for EmDash CMS — monorepo.
Manager / install guide: FREEFORM-REGISTRY-GUIDE.md — verify publish, install on a site, links.
Every day — two terminals:
# 1 — demo site → http://localhost:4321/demo
cd emdash-app && pnpm run dev:clean
# 2 — plugin rebuild on save (optional)
cd freeform && pnpm run build:watchemdash-freeform/
freeform/ EmDash registry plugin
freeform-astro/ Astro integration + component library
emdash-freeform-mcp/ Standalone Cloudflare Worker MCP server
emdash-app/ Mars Rover Supply demo site (EmDash + Freeform consumer)
PLAN.md Phased product plan with progress tracking
LIMITATIONS.md Known limitations and EmDash feature requests
FREEFORM-POC.md Original POC handoff — architecture decisions and gotchas
The core plugin. Runs in EmDash's V8 sandbox (format: "standard"). Manages forms, submissions, email notifications, webhooks, spam scoring, and CSV exports via Block Kit admin UI and a full set of plugin routes.
Install (for site owners): see freeform/README.md — enable the registry in astro.config.mjs, install freeform from Plugins → Registry, then add @solspace/freeform-astro on the frontend.
Build locally:
cd freeform
pnpm install
pnpm run build # compile src/ → dist/
pnpm run bundle # produces dist/freeform-x.x.x.tar.gzDevelopment: see Daily dev — start here every time in Development setup.
Publish to registry: see Publishing (pnpm dlx @emdash-cms/plugin-cli@0.5.1 login must run outside the monorepo).
See Publishing for CI via freeform/v* tags.
Astro integration and component library. Injects 6 Freeform-specific routes into any Astro site and provides unstyled FreeformForm and FreeformChat components.
Install:
pnpm add @solspace/freeform-astroUsage:
// astro.config.mjs
import freeformAstro from "@solspace/freeform-astro"
export default defineConfig({ integrations: [emdash(), freeformAstro()] })---
import { FreeformForm } from "@solspace/freeform-astro/components"
---
<FreeformForm formId="contact" />Components ship unstyled. Style them by targeting data-freeform-* attributes — see emdash-app/src/pages/contact.astro for a full dark-theme example.
Standalone Cloudflare Worker that exposes 25 MCP tools to AI agents (Claude Desktop, Cursor, etc.). Each customer deploys their own Worker instance pointed at their EmDash site.
Deploy:
cd emdash-freeform-mcp
pnpm install
# Edit wrangler.jsonc — set your Worker name and custom domain
wrangler secret put EMDASH_SITE_URL
wrangler deploySee emdash-freeform-mcp/README.md for full setup instructions including Claude Desktop config.
Mars Rover Supply — a demo EmDash site that consumes all three packages. Used for development and as a reference implementation.
The demo site is NOT published. It exists to validate the packages and demonstrate real-world usage.
Requirements: Node.js ≥ 22.12, pnpm ≥ 11 (see root package.json).
git clone https://github.com/solspace/emdash-freeform
cd emdash-freeform
pnpm install
# Build the plugin once (required before the demo site can load Freeform)
cd freeform && pnpm run buildUse two terminals from the repo root.
Terminal 1 — demo site (keep running)
cd emdash-app
pnpm run dev:cleandev:clean builds the Freeform plugin first, clears Vite’s cache, and runs npx emdash dev (migrations + Astro on port 4321). Wait until you see astro … ready, then open:
| URL | What |
|---|---|
| http://localhost:4321/demo | Freeform POC (contact form) |
| http://localhost:4321/contact | Styled form example |
| http://localhost:4321/_emdash/admin | EmDash admin → Freeform |
On the first start after a cache clear, the terminal may log one emdash/middleware optimize + program reload — that’s normal. If a page shows Astro is not defined, wait a few seconds and refresh once.
Terminal 2 — plugin watch (while editing freeform/)
cd freeform
pnpm run build:watchRebuilds dist/ on save; refresh the browser or admin after changes.
If the dev server acts up (wrong port, stale errors): stop all dev processes, then run pnpm run dev:clean again in emdash-app. Only one server should use port 4321.
Plain start (no cache clear — use when yesterday’s dev session was fine):
cd emdash-app && npx emdash devReleases are tag-driven GitHub Actions workflows. Push a scoped tag and CI builds + publishes automatically.
| Tag pattern | Package | Destination |
|---|---|---|
freeform/v* |
EmDash plugin | Plugin registry |
freeform-astro/v* |
@solspace/freeform-astro |
npm |
Sites install registry plugins with experimental.registry: "https://registry.emdashcms.com" in astro.config.mjs.
In the repo → Settings → Secrets and variables → Actions, add:
| Secret | How to get it |
|---|---|
EMDASH_PLUGIN_OAUTH_SESSIONS |
After login (see below), copy the full contents of ~/.emdash/oauth/sessions.json. CI also derives ~/.emdash/credentials.json from this file (publish checks both). |
NPM_TOKEN |
npmjs.com → Access Tokens → granular token with read/write on @solspace/* |
REGISTRY_TARBALL_URL (optional) |
Override the public tarball URL passed to emdash-plugin publish. By default CI publishes the tarball to npm as @solspace/freeform-plugin-dist and uses its unpkg URL (works with a private GitHub repo). |
The GitHub repo can stay private for source code — you do not need to make it public. Registry publish needs a public HTTPS URL for the tarball; CI mirrors each release to @solspace/freeform-plugin-dist on npm (same NPM_TOKEN as freeform-astro) and registers a jsDelivr/unpkg URL with the EmDash registry. CDNs can lag npm by a few minutes after first publish; re-run the workflow if that step times out. GitHub Releases are still created for your own archive. Override with REGISTRY_TARBALL_URL if you host the tarball elsewhere (R2, S3, etc.).
-
Bump the version in the package you are shipping:
- Registry plugin:
freeform/package.jsonandfreeform/src/index.ts(versionfield) - npm package:
freeform-astro/package.json - Update
freeform/CHANGELOG.mdwhen releasing the plugin - Add or update
freeform-astro/README.mdwhen releasing the Astro package (npm displays this on the package page)
- Registry plugin:
-
Commit and push to
main(mergedev→mainfirst if needed). -
Tag and push (one tag per package, or both for a combined release):
# EmDash plugin registry
git tag freeform/v0.1.0
git push origin freeform/v0.1.0
# npm Astro package
git tag freeform-astro/v0.1.1
git push origin freeform-astro/v0.1.1-
Watch Actions in GitHub for the publish workflow result.
-
Verify:
- Registry:
npx emdash-plugin search freeformor EmDash admin → Plugins → Registry - npm:
npm view @solspace/freeform-astro
- Registry:
If bundle and GitHub Release succeeded but Publish release record to registry failed, re-run that job (or step) from Actions — you do not need a new tag.
If the workflow failed earlier (e.g. bundle), delete the tag and push it again on the fixed commit:
git tag -d freeform-astro/v0.1.1
git push origin :refs/tags/freeform-astro/v0.1.1
git tag freeform-astro/v0.1.1
git push origin freeform-astro/v0.1.1If the version already exists on npm or the registry, bump the version — you cannot republish the same semver.
Registry plugin:
Log in outside the monorepo (workspace dependency hoisting breaks emdash-plugin login here):
pnpm dlx @emdash-cms/plugin-cli@0.5.1 login solspace.com
cat ~/.emdash/oauth/sessions.json # → paste into EMDASH_PLUGIN_OAUTH_SESSIONS secretThen bundle and publish:
cd freeform
pnpm run bundle
pnpm dlx @emdash-cms/plugin-cli@0.5.1 publish \
--url https://your-host/freeform-x.y.z.tar.gz \
--local dist/freeform-x.y.z.tar.gznpm package:
cd freeform-astro
npm login # first time
pnpm publish --access public --no-git-checks --dry-run # test
pnpm publish --access public --no-git-checks # publish