HarborHarbor
DocumentationGuidesPlugins
Recipes

Public Intake Form

Public feedback form that drops submissions into Linear. (Beta)

Beta

Uses Apps with auth: "public" which are part of the Apps beta surface.

Anonymous users can submit feedback at a public URL; their submissions become Linear issues in your team's backlog. Total infrastructure: zero servers, two TypeScript files.

The submit Function (internal-write, called by the public route)

feedback-submit.ts

defineJob({
  name: "feedback-submit",
  description: "Drop a feedback submission into Linear.",
  handler: async (ctx, input: {
    subject: string; body: string; email?: string;
  }) => {
    const subject = input.subject.trim().slice(0, 200);
    const body    = input.body.trim().slice(0, 5_000);
    if (!subject || !body) {
      return { ok: false, error: "subject and body required" };
    }

    const ticket = await ctx.plugins.linearMcp.createIssue({
      teamId:      "EXT-FEEDBACK",
      title:       subject,
      description: [
        body,
        "",
        "---",
        input.email ? \`From: \${input.email}\` : "From: anonymous",
      ].join("\n"),
    });

    return { ok: true, ticket: ticket.id };
  },
});

The public App

feedback-app.ts

deployApp({
  name: "feedback",
  description: "Public feedback intake.",
  jobs: { submit: "feedback-submit" },
  routes: {
    "GET /":  {
      auth: "public",
      job:  "feedback-submit",            // referenced only for typing
      render: () => formPage({
        title:  "Send us feedback",
        fields: [
          { name: "subject", label: "Subject", required: true },
          { name: "body",    label: "Details", required: true, type: "textarea" },
          { name: "email",   label: "Your email (optional)", type: "email" },
        ],
        submitTo: "POST /",
      }),
    },
    "POST /": { auth: "public", job: "submit" },
  },
});
hrbr inspect -f ./feedback-submit.job.ts
hrbr inspect -f ./feedback-app.app.ts
# → https://feedback.<workspace>.tryharbor.ai

The trust pattern

The public route ONLY collects + writes to orbit.db / Linear via the internal feedback-submit Function. The public route does not have direct access to plugin tokens — the Function owns the upstream credential.

The CRUCIAL rule for public routes: collect-only or read-only. Never expose a Function with destructive plugin scope behind auth: "public". The meta skill harbor-apps encodes this rule for agents.

Hardening

  • Rate-limit via a step.do that consults orbit.cache for "this IP in the last minute" before writing.
  • Spam filter with orbit.ai.classify({ labels: ["legit", "spam"] }) before creating the Linear ticket.
  • Captcha — embed an external captcha widget in the formPage HTML and verify the token in the submit handler.