Publish your first feed
Updated 17 August 2026
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.
If you have a coding agent
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.
npx skills add Cabuya/cabuya-skill
# or, without the installer:
git clone https://github.com/Cabuya/cabuya-skill .agents/skills/cabuyaIf you are doing it by hand
Five steps. Step 3 is the most common point of failure, and is worth reading in full.
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.
{
"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"
}
]
}
- 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.
{
"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.
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.jsonPlace 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.jsonStatic 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.jsonRegister the route before the SPA fallback, or drop the file under public/.well-known/ — Laravel serves that directory directly.
public/.well-known/cabuya.jsonAdd 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.jsonAdd a static route for /.well-known/ before the catch-all urlpattern. Order in urlpatterns is the entire mechanism.
Upload the file and then request it. Several hosts hide dot-directories by default, and the deploy will not warn you.
/.well-known/cabuya.jsonOne 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.
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: *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: *Merge the headers array into an existing vercel.json rather than replacing the file.
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Access-Control-Allow-Origin", "value": "*" }
]
}
]
}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 *;
}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>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 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_personnombrenombresapellidoapellidosphonetelefonoteléfonocelularmovilmóvilwhatsappwaemailcorreomailcedulacéduladocumentodninit_personadireccion_casafotophotocontactocontact_phonecontact_emailresponsableencargadobeneficiariobeneficiaryvictimavíctimadesaparecidomissing_person
Value shapes it flags wherever they appear
email-addresscolombian-mobileintl-phonewhatsapp-linknational-id
Run it
Point the validator at your manifest URL. It follows the feeds it declares, and reports what it found.
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:
npx @cabuya/validator validate https://example.org/.well-known/cabuya.jsonHow 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.