Publishing to the Hub
Share your work with the Aerostack community by publishing to the Hub. You can publish via the Admin dashboard, the CLI, GitHub Actions, or GitLab CI.
Prerequisites
Section titled “Prerequisites”Before you publish:
- Install the Aerostack CLI
- Log in with
aerostack login(you need an account key from app.aerostack.dev/account/keys)
Function structure
Section titled “Function structure”Every publishable function lives in its own directory with an aerostack.json config and at least one source file.
Directorymy-function
- aerostack.json
- index.ts
- README.md
The aerostack.json config file
Section titled “The aerostack.json config file”This file defines your function’s identity, version, and capabilities. It is read by the CLI, CI pipelines, and the Hub.
{ "name": "image-resizer", "version": "1.0.0", "description": "Resize and transform images on the edge using Cloudflare Workers.", "category": "media", "language": "typescript", "runtime": "cloudflare-worker", "license": "MIT", "tags": ["image", "media", "resize", "cdn"], "entrypoint": "index.ts", "routePath": "/api/image-resize", "routeExport": "imageResizerRoute", "npmDependencies": [], "envVars": ["IMAGE_MAX_WIDTH", "IMAGE_QUALITY"], "drizzleSchema": false}Field reference
Section titled “Field reference”| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Slug-friendly name. Becomes the Hub URL: hub.aerostack.dev/functions/you/image-resizer |
version | string | Yes | SemVer string, e.g. 1.0.0 |
description | string | Yes | Short description shown in search results |
category | string | Yes | One of: auth, payments, media, email, ai, database, utility, analytics, notifications, storage |
language | string | Yes | typescript or javascript |
runtime | string | Yes | cloudflare-worker (default) |
license | string | Yes | SPDX identifier, e.g. MIT, Apache-2.0 |
tags | string[] | No | Search keywords, max 10 |
entrypoint | string | Yes | Path to main source file relative to the function directory |
routePath | string | No | Default HTTP route when installed, e.g. /api/image-resize |
routeExport | string | No | Named export from your adapter, e.g. imageResizerRoute |
npmDependencies | string[] | No | npm packages the consumer must install |
envVars | string[] | No | Environment variables the consumer must set |
drizzleSchema | boolean | No | Set true if you export a Drizzle table schema |
Writing your function
Section titled “Writing your function”Your index.ts should export a Hono route handler. This is the convention the CLI bundles and the Hub displays.
import { Hono } from 'hono';
export const imageResizerRoute = new Hono<{ Bindings: { DB: D1Database } }>();
imageResizerRoute.get('/resize', async (c) => { const url = c.req.query('url'); const width = parseInt(c.req.query('width') ?? '800');
if (!url) { return c.json({ error: 'url query param required' }, 400); }
return c.json({ original: url, resized: `${url}?width=${width}&format=webp`, width, });});
imageResizerRoute.get('/health', (c) => c.json({ module: 'image-resizer', ok: true }));Choose a publishing method
Section titled “Choose a publishing method”The CLI is the fastest way to publish. Push source code to create a draft, then publish by ID.
Step 1 — Push to create a draft
aerostack functions push ./my-function/index.tsEach push creates a new draft. The command outputs the function ID, slug, and a link to the Admin. Save the ID.
Example output:
Pushing function 'image-resizer' to Aerostack...Pushed successfully! Slug: image-resizer Status: draft Admin URL: https://app.aerostack.dev/functions/edit/<id>Step 2 — Publish the draft
aerostack functions publish <id>The function is now live on the Hub and can be installed with aerostack functions install.
Step 3 — (Optional) Finalize in the Admin
Before publishing, you can open the Admin URL from the push output to add a README, screenshots, and tags. Then click Publish from the Admin instead.
Updating an existing function via CLI
Section titled “Updating an existing function via CLI”Each aerostack functions push creates a new draft — it does not update an existing function by slug. To update an existing function:
- Admin: Open the function in Admin and edit directly.
- CI: Use the GitHub Actions or GitLab CI workflow (below). The CI endpoint upserts by slug, so it updates the same function.
Set up a GitHub Actions workflow so that every push to main (or every tagged release) automatically publishes your function to the Hub.
Step 1 — Add your API key as a GitHub secret
In your GitHub repo, go to Settings > Secrets and variables > Actions and create a secret named AEROSTACK_API_KEY with your ac_secret_xxxx key.
Step 2 — Add the workflow file
Create .github/workflows/publish-aerostack.yml:
name: Publish to Aerostack Hub
on: push: branches: - main
jobs: publish: name: Publish Function runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4
- name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20'
- name: Read aerostack.json id: config run: | echo "config=$(cat aerostack.json | tr -d '\n')" >> $GITHUB_OUTPUT ENTRYPOINT=$(cat aerostack.json | jq -r '.entrypoint // "index.ts"') echo "entrypoint=$ENTRYPOINT" >> $GITHUB_OUTPUT
- name: Read source file id: source run: | ENTRYPOINT="${{ steps.config.outputs.entrypoint }}" CODE=$(cat "$ENTRYPOINT" | jq -Rs .) echo "code=$CODE" >> $GITHUB_OUTPUT
- name: Publish to Aerostack run: | CONFIG='${{ steps.config.outputs.config }}' CODE=${{ steps.source.outputs.code }}
PAYLOAD=$(jq -n \ --argjson config "$CONFIG" \ --argjson code "$CODE" \ '$config + {code: $code, publish: true}')
RESPONSE=$(curl -s -w "\n%{http_code}" \ -X POST https://api.aerostack.dev/api/community/functions/ci/publish \ -H "X-API-Key: ${{ secrets.AEROSTACK_API_KEY }}" \ -H "Content-Type: application/json" \ -d "$PAYLOAD")
HTTP_CODE=$(echo "$RESPONSE" | tail -1) BODY=$(echo "$RESPONSE" | head -1)
if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ]; then echo "Publish failed with HTTP $HTTP_CODE" echo "$BODY" exit 1 fi
echo "Published successfully!" echo "$BODY" | jq . env: AEROSTACK_API_KEY: ${{ secrets.AEROSTACK_API_KEY }}name: Publish to Aerostack Hub
on: release: types: [published]
jobs: publish: name: Publish on Release runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4
- name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20'
- name: Update version from git tag run: | TAG="${{ github.ref_name }}" VERSION="${TAG#v}" echo "Publishing version: $VERSION" jq --arg v "$VERSION" '.version = $v' aerostack.json > tmp.json mv tmp.json aerostack.json
- name: Read aerostack.json id: config run: | echo "config=$(cat aerostack.json | tr -d '\n')" >> $GITHUB_OUTPUT ENTRYPOINT=$(cat aerostack.json | jq -r '.entrypoint // "index.ts"') echo "entrypoint=$ENTRYPOINT" >> $GITHUB_OUTPUT
- name: Read source file id: source run: | ENTRYPOINT="${{ steps.config.outputs.entrypoint }}" CODE=$(cat "$ENTRYPOINT" | jq -Rs .) echo "code=$CODE" >> $GITHUB_OUTPUT
- name: Publish to Aerostack run: | CONFIG='${{ steps.config.outputs.config }}' CODE=${{ steps.source.outputs.code }}
PAYLOAD=$(jq -n \ --argjson config "$CONFIG" \ --argjson code "$CODE" \ '$config + {code: $code, publish: true}')
RESPONSE=$(curl -s -w "\n%{http_code}" \ -X POST https://api.aerostack.dev/api/community/functions/ci/publish \ -H "X-API-Key: ${{ secrets.AEROSTACK_API_KEY }}" \ -H "Content-Type: application/json" \ -d "$PAYLOAD")
HTTP_CODE=$(echo "$RESPONSE" | tail -1) BODY=$(echo "$RESPONSE" | head -1)
if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ]; then echo "Publish failed with HTTP $HTTP_CODE" echo "$BODY" exit 1 fi
echo "Published version ${{ github.ref_name }}!" echo "$BODY" | jq . env: AEROSTACK_API_KEY: ${{ secrets.AEROSTACK_API_KEY }}Monorepo with multiple functions
Section titled “Monorepo with multiple functions”If your repo contains multiple function directories, scope the workflow trigger:
on: push: branches: [main] paths: - 'functions/my-rate-limiter/**'And adjust the working directory in each step to point to the correct function folder.
Use a GitLab CI pipeline to publish on every push to main or on a tagged release.
Step 1 — Add your API key as a CI variable
In your GitLab project, go to Settings > CI/CD > Variables and add AEROSTACK_API_KEY with your ac_secret_xxxx key. Enable Mask variable to hide it in logs.
Step 2 — Add the pipeline file
Create .gitlab-ci.yml at the root of your function repository:
stages: - publish
publish-to-aerostack: stage: publish image: node:20-alpine before_script: - apk add --no-cache curl jq script: - | CONFIG=$(cat aerostack.json) ENTRYPOINT=$(echo "$CONFIG" | jq -r '.entrypoint // "index.ts"') CODE=$(cat "$ENTRYPOINT" | jq -Rs .)
PAYLOAD=$(jq -n \ --argjson config "$CONFIG" \ --argjson code "$CODE" \ '$config + {code: $code, publish: true}')
echo "Publishing $(echo "$CONFIG" | jq -r '.name') v$(echo "$CONFIG" | jq -r '.version')..."
RESPONSE=$(curl -s -w "\n%{http_code}" \ -X POST "https://api.aerostack.dev/api/community/functions/ci/publish" \ -H "X-API-Key: $AEROSTACK_API_KEY" \ -H "Content-Type: application/json" \ -d "$PAYLOAD")
HTTP_CODE=$(echo "$RESPONSE" | tail -1) BODY=$(echo "$RESPONSE" | head -1)
if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ]; then echo "Publish failed with HTTP $HTTP_CODE" echo "$BODY" exit 1 fi
echo "Published successfully!" echo "$BODY" | jq . only: - mainstages: - publish
publish-to-aerostack: stage: publish image: node:20-alpine before_script: - apk add --no-cache curl jq script: - | VERSION="${CI_COMMIT_TAG#v}" echo "Publishing version: $VERSION"
jq --arg v "$VERSION" '.version = $v' aerostack.json > tmp.json mv tmp.json aerostack.json
CONFIG=$(cat aerostack.json) ENTRYPOINT=$(echo "$CONFIG" | jq -r '.entrypoint // "index.ts"') CODE=$(cat "$ENTRYPOINT" | jq -Rs .)
PAYLOAD=$(jq -n \ --argjson config "$CONFIG" \ --argjson code "$CODE" \ '$config + {code: $code, publish: true}')
RESPONSE=$(curl -s -w "\n%{http_code}" \ -X POST "https://api.aerostack.dev/api/community/functions/ci/publish" \ -H "X-API-Key: $AEROSTACK_API_KEY" \ -H "Content-Type: application/json" \ -d "$PAYLOAD")
HTTP_CODE=$(echo "$RESPONSE" | tail -1) BODY=$(echo "$RESPONSE" | head -1)
if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ]; then echo "Publish failed with HTTP $HTTP_CODE" echo "$BODY" exit 1 fi
echo "Published $VERSION successfully!" echo "$BODY" | jq . only: - tagsMulti-function monorepo
Section titled “Multi-function monorepo”Use the changes keyword to only trigger publishing for modified functions:
publish-rate-limiter: stage: publish script: - cd functions/rate-limiter && # publish commands here only: changes: - functions/rate-limiter/** refs: - mainThe Admin dashboard provides a browser-based editor for creating and publishing functions with no local setup required.
Step 1 — Create a new function
Log in to the Admin dashboard and go to My Functions. Click Create to start a new function.
Step 2 — Write your code
Use the built-in Monaco editor to write your function logic. Add a name, description, category, and tags.
Step 3 — Publish
Click Publish to make the function live on the Hub. You can also save as a draft first and publish later.
The Admin is also useful for finalizing drafts created via the CLI — add a README, adjust tags, then click Publish.
How CI publishing works
Section titled “How CI publishing works”The CI endpoint (POST /api/community/functions/ci/publish) behaves differently from the CLI:
- Upserts by slug — If a function with the same name already exists under your account, it updates the code and metadata. Otherwise, it creates a new function.
publish: truemakes the function live immediately. Setpublish: falseto land as a draft for review.- Version snapshots are permanent — Every published version is preserved. Consumers who installed an earlier version can still reference it.
Common errors
Section titled “Common errors”| Error | Cause | Fix |
|---|---|---|
INVALID_CATEGORY | Category not in allowed list | Check spelling, use lowercase |
MISSING_ENTRYPOINT | Entrypoint file not found | Verify path is relative to the function directory |
EMPTY_CODE | Entrypoint file is empty | Add at least one exported function |
DUPLICATE_SLUG | Slug exists under a different account | Rename your function |
UNAUTHORIZED | API key invalid or expired | Run aerostack login again |
VERSION_CONFLICT | Version string already published | Bump version in aerostack.json |
Version history
Section titled “Version history”Every time you publish a new version, a code snapshot is stored. All past versions are preserved — consumers who installed an earlier version can still reference its code.
To release an update, bump the version field in aerostack.json and publish again. There is no way to overwrite a past version.
Reputation
Section titled “Reputation”Every time your function is installed (via aerostack functions install or the Hub), you earn Community Reputation. Higher reputation unlocks badges and better visibility in search results.