# Publish your first feed

Publish your first Cabuya feed. Two files, one header, one validator run, one registry entry. Copy-paste in five minutes; a real mapping is an afternoon.

Canonical: https://cabuya.org/developers/quickstart
Language: en

## If you have a coding agent

Two files, one response header and a validator run. Everything below is copy-paste ready and conforms to the specification it teaches — the blocks on this page are checked by the test suite against the real validator.

Install the skill and tell it to publish a Cabuya feed. The skill vendors the specification, so it works without network access and without guessing at field names.

> **Diagram.** An afternoon, step by step. Each step produces a file or a result you can inspect.
>
> Five steps: write the manifest, export one feed, run the validator, fix what it reports, then open a registry pull request. The first three are typically under an hour; the whole path is an afternoon for a small application.

## Save this first

This is a complete, conforming manifest. Replace the two URLs and the publisher id with your own, and step 1 is done.

```json
{
  "protocol": {
    "name": "cabuya",
    "spec_version": "0.1.0"
  },
  "publisher": {
    "publisher_id": "example-app",
    "canonical_url": "https://example.org"
  },
  "conformance_target": "L2",
  "license": "CC-BY-4.0",
  "permitted_use": [
    "display",
    "aggregate"
  ],
  "feeds": [
    {
      "name": "places",
      "url": "https://example.org/feeds/places.json",
      "entity": "place",
      "profile": "core"
    }
  ]
}
```

## Publish your first feed

**If you are doing it by hand** — Five steps. Step 3 is the most common point of failure, and is worth reading in full.

1. **Write the manifest** — It says who you are, what you publish and under which licence. Twelve lines is a real one, not a stub.
2. **Put it at /.well-known/cabuya.json** — The path is fixed. Consumers look there and nowhere else, so there is no discovery step to implement.
3. **Exclude that path from your catch-all** — Not optional. If your framework serves index.html for unknown paths, your manifest returns 200 with HTML, and every consumer treats it as absent.
4. **Serialize your places into the envelope** — The envelope carries five fields and an array. Map what you already have; publish null for anything nobody has actually confirmed.
5. **Run the validator until it reports no findings, then open a registry entry** — Every finding names the field and states the fix. When the run is clean, one pull request adds you to the registry.

## places.json

```json
{
  "last_updated": "2026-01-01T00:00:00Z",
  "ttl": 300,
  "version": "0.1.0",
  "publisher_id": "example-app",
  "license": "CC-BY-4.0",
  "permitted_use": [
    "display",
    "aggregate"
  ],
  "attribution": "Your App",
  "data": {
    "places": [
      {
        "id": "1",
        "publisher_id": "example-app",
        "name": "Coliseo Municipal",
        "place_kind": "shelter",
        "municipality_code": "66001",
        "address_text": "Avenida Ejemplo 12-34",
        "lat": 4.8133,
        "lon": -75.6961,
        "lifecycle_status": "active",
        "service_status": "open",
        "last_confirmed_at": null,
        "source": {
          "source_id": "example-app"
        },
        "public_url": "https://example.org/places/1"
      }
    ]
  }
}
```

## Step 3, per stack

Find yours, apply the single line, then request the URL and confirm that the response is JSON.

Four of the twenty applications in the founding analysis failed here, and all four believed they had published. The manifest was reachable, returned 200, and contained their homepage.

- **Next.js** — Files under `public/` are served before the catch-all route, so placement is the whole fix. Verify anyway: a custom `rewrites` entry can still capture it. (`public/.well-known/cabuya.json`)
- **Vite / React SPA** — Place the file in `public/`, and exclude `/.well-known/*` from the SPA rewrite in your host config — the rewrite is what serves index.html for unknown paths. (`public/.well-known/cabuya.json`)
- **Astro** — Static output has no catch-all, so the file is served as-is. On an SSR adapter, confirm the middleware does not rewrite unmatched paths. (`public/.well-known/cabuya.json`)
- **Laravel** — Register the route before the SPA fallback, or drop the file under `public/.well-known/` — Laravel serves that directory directly. (`public/.well-known/cabuya.json`)
- **PHP / Apache** — Add `RewriteCond %{REQUEST_URI} !^/\.well-known/` above the front-controller rule in `.htaccess`. Without it the router answers, with a 200 and HTML. (`public/.well-known/cabuya.json`)
- **Django** — Add a static route for `/.well-known/` before the catch-all urlpattern. Order in `urlpatterns` is the entire mechanism.
- **Static host** — Upload the file and then request it. Several hosts hide dot-directories by default, and the deploy will not warn you. (`/.well-known/cabuya.json`)

## One header, which a file cannot carry

Two static files are schema-valid and are not yet L2. The feed also has to be readable from a browser, and that is a response header your host sets — not something you can put inside the JSON.

> The specification calls this the one non-obvious MUST. Without `Access-Control-Allow-Origin: *`, any consumer running in a browser needs a server-side proxy to read you — so the participants with the lowest cost to build are exactly the ones locked out. The validator reports it as ENV007, and it is the check a publisher who did everything else right fails first.

**Cloudflare Pages** — `_headers`

A `_headers` file at the root of the published output. Cloudflare applies it at the edge, so nothing in your build has to know about it.

```
/*
  Access-Control-Allow-Origin: *
```

**Netlify** — `_headers`

The same `_headers` format, in the publish directory. Netlify ignores the file if it is outside it, which is the usual reason a correct file has no effect.

```
/*
  Access-Control-Allow-Origin: *
```

**Vercel** — `vercel.json`

Merge the `headers` array into an existing `vercel.json` rather than replacing the file.

```
{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "Access-Control-Allow-Origin", "value": "*" }
      ]
    }
  ]
}
```

**nginx**

Scope it to the manifest and the feeds rather than to the whole server. `add_header` inside a `location` block replaces any inherited headers, so declare it where it applies.

```
location /.well-known/cabuya.json {
  add_header Access-Control-Allow-Origin *;
}

location /feeds/ {
  add_header Access-Control-Allow-Origin *;
}
```

**Apache** — `.htaccess`

Needs `mod_headers` enabled. On shared hosting it usually is; if the header does not appear, that module is the first thing to check.

```
<FilesMatch "cabuya\.json$|\.json$">
  Header set Access-Control-Allow-Origin "*"
</FilesMatch>
```

**Amazon S3 + CloudFront**

The bucket CORS configuration. CloudFront must also be told to forward the `Origin` header, or it caches one response for every origin and the header never varies.

```
{
  "CORSRules": [
    {
      "AllowedOrigins": ["*"],
      "AllowedMethods": ["GET", "HEAD"],
      "AllowedHeaders": ["*"]
    }
  ]
}
```

**GitHub Pages**

GitHub Pages does not let you set response headers, and it already sends `Access-Control-Allow-Origin: *` on every response. Nothing to do — but nothing you can do either, if that ever changes.


## Before you publish: the one decision that is yours

Cabuya carries places, not people. Review the fields you are about to map and confirm that none of them holds a person’s name, a personal phone or email, an individual case, or a moderation verdict about an individual.

Field names the validator rejects outright: name_person, nombre, nombres, apellido, apellidos, phone, telefono, teléfono, celular, movil, móvil, whatsapp, wa, email, correo, mail, cedula, cédula, documento, dni, nit_persona, direccion_casa, foto, photo, contacto, contact_phone, contact_email, responsable, encargado, beneficiario, beneficiary, victima, víctima, desaparecido, missing_person.

Value shapes it flags wherever they appear: email-address, colombian-mobile, intl-phone, whatsapp-link, national-id.

A person makes this decision once, when the mapping is written. The validator checks it on every run afterwards, and it reports the field, never the value it found.

## Run it

Point the validator at your manifest URL. It follows the feeds it declares, and reports what it found.

```sh
npx @cabuya/validator validate https://example.org/.well-known/cabuya.json
```

The paste mode runs the same engine in your browser, with nothing uploaded. URL checking — which also measures the transport behaviour — needs a server, and that part is still being built. The command line does both today:

## How long this actually takes

Five minutes is the honest number for the path above: two static files and one header, copied, edited and uploaded. It is a real conformance level — L2 — and it is genuinely useful to consumers.

Mapping a live database into the schema is an afternoon, sometimes two: your statuses have to be reconciled with the shared vocabulary, and somebody has to decide what your data actually means. That work is not avoidable, and stating it here is better than leaving it to surface at step 4.

## Site Navigation

- [Home](https://cabuya.org/)
- [Specification](https://cabuya.org/developers/spec)
- [Schemas](https://cabuya.org/developers/schemas)
- [RFCs](https://cabuya.org/rfcs)
- [Changelog](https://cabuya.org/changelog)
- [Developers](https://cabuya.org/developers)
- [Registry](https://cabuya.org/registry)
- [Governance](https://cabuya.org/governance)
- [Join](https://cabuya.org/join)
- [GitHub](https://github.com/Cabuya)
- [Agent skill repository](https://github.com/Cabuya/cabuya-skill)
- [Founding record](https://github.com/Cabuya/cabuya.org/tree/main/docs/context)
