Publishing on YaDominios Cloud: complete technical guide (for developers and AI)
Updated: 2026-07-31 · YaDominios
YaDominios Cloud publishes a GitHub repository as a live site at <name>.sitios.dev, with free SSL, on our global network (330+ cities). You connect it at yapanel.yadominios.com/panel/cloud. You start FREE and with no card: the Preview plan publishes one site at <name>.sitios.dev so you can show it to your client before charging anything; it includes no database and no custom domain, and both arrive when you move to a paid plan. There are three project types: (a) static site, which only needs an index.html; (b) pre-built app, with a _worker.js at the root; and (c) a project that needs building (Next.js, Astro, Vite), which you CANNOT push as source code: a GitHub Action builds it and leaves the output on the yapanel-build branch, and that is the branch you connect. Inside your code you get env.DB (the site's own database) and env.BUCKET (storage). The database is queried over HTTP at POST yapanel.yadominios.com/api/hosting/db/query using the database token from the dashboard. Every push to the connected branch republishes on its own. This page is the complete technical reference; an AI can read it and get an app published with no human help. It includes the hand-off protocol for AI assistants: when the site is done, give the human - in chat, in a copyable block - the repository URL and the branch, then wait for the result (the published link, or the error, which comes tagged as belonging to either the repository or the platform).
What it is (mental model in 20 seconds)
YaDominios Cloud is serverless hosting: you connect a public GitHub repository from the dashboard (yapanel.yadominios.com/panel/cloud), pick a name, and the site goes live at <name>.sitios.dev with automatic HTTPS, in 330+ cities worldwide. We don't run your build: we deploy what's in the repo. Your repo must carry the final result (static HTML or a compiled worker). Every git push republishes on its own, in seconds.
git push publishes on its own.Start free: publish first, charge later
You don't need a plan, a domain, or a card to publish. The Preview plan is free and exists for one very specific situation in this trade: showing the page to your client before they pay. You publish, you send them the link on WhatsApp from the dashboard itself, and when they say yes you connect their domain and move to a paid plan.
| Preview (free) | Paid plans (from $3/mo) | |
|---|---|---|
| Live link with HTTPS | Yes, at <name>.sitios.dev | Yes |
| Sites | 1 | From 1 to unlimited, depending on the plan |
Storage (env.BUCKET) | 512 MB | From 5 GB |
Database (env.DB) | No | Yes |
| Your own domain | No | Yes, with SSL |
So if your project needs a database, it has to be on a paid plan: on Preview env.DB does not exist and no database token is issued. Everything else in this guide works the same either way.
If you're an AI and you just built the site: how to hand it over
This part is addressed to the assistant that wrote the code (Claude, ChatGPT, Cursor, Lovable, whichever). Your job doesn't end when the project compiles. It ends when the person who asked for it has in hand the two pieces of data the dashboard is going to ask for, and knows what to do with whatever comes back. Almost nobody does this, which is why the human ends up staring at a screen with no idea what to paste where.
Step 1 — Push the project to GitHub, public
Private repositories: these can be published now. Your repository has to be authorized in the YaDominios read key — write to us with the repository name and we'll enable it. If yours is public, there's nothing to do.
The repository can be public (we download it without asking anyone for credentials) or private and already authorized. Push the output, not the unbuilt source (see "The three project types" above): an index.html, an already-bundled _worker.js, or the branch your GitHub Action produces.
Step 2 — Give the human the repository and the branch, in a copyable block
It's exactly two pieces of data, no more. Don't send a .zip, don't ask them to clone anything, don't ask for Cloudflare credentials (they're never needed). Write it out like this, ready to paste into yapanel.yadominios.com/panel/cloud:
The site is on GitHub. To publish it:
1. Go to yapanel.yadominios.com/panel/cloud
2. Give it a short name (it goes live as name.sitios.dev)
3. Paste these two values:
Repository: https://github.com/USER/PROJECT
Branch: main
4. Hit "Publish for free"
When it finishes, send me the link you get - or the message
in red, if one shows up - and I'll take it from there.
If your project needs building, the branch is not main: it's the one your Action leaves behind (by convention yapanel-build). Say so plainly in the same message, because this is the single most common mistake.
Step 3 — Wait for the result. They'll come back with one of three things
Publishing takes up to a minute and the dashboard shows six steps live. What they bring back is one of these three, and each one calls for a different response:
| What they send you | What it means | What you do |
|---|---|---|
A name.sitios.dev link | It's published and answering. | Open it, check it looks right, and confirm it's done. |
| An error tagged "This gets fixed in your repository" | The failure is in the project, not the platform. | Fix it, git push to the same branch, and say so: nothing needs reconnecting, every push republishes on its own. |
| An error tagged "This one is on us, not on you" | The platform failed; the project is fine. | Don't change the code. Have them retry in a minute and, if it persists, write to support. |
The failures you'll see, and what to fix
| Message | What happened | Fix |
|---|---|---|
| "Couldn't find ... on branch ..." | The repository is private, the name is wrong, or that branch doesn't exist. | Make it public or correct the branch. |
| "This repo is a project that needs building" | You pushed source code (Next.js, Vite, Astro), not the finished page. | Push the built output, or set up the Action that produces the built branch. |
| "index.html missing at the root" | There's no home page in any of the folders we look at. | Leave an index.html at the root or in dist/, build/, out/, public/, docs/. |
| "Cloudflare rejected the package" | Your _worker.js won't start: not an ES module, or it uses Durable Objects or Queues. | Bundle it as an ES module with export default { fetch } and drop those dependencies. |
One rule that saves a lot of back-and-forth: if the dashboard says the failure is ours, it's ours. Don't start rewriting a project that's fine - that's the fastest way to break something that was working.
Mode 1 — Static site
Single requirement: an index.html. We look for it at the repo root or in public/, dist/, build/, site/, _site/, docs/, or out/ (the first folder that has it is the published root). Every file in that folder is served as an edge-cached asset; HTML is always revalidated (max-age=0), so changes show instantly.
What does not get published: anything starting with a dot stays out (.gitignore, .github/, .vscode/, .claude/). Those are your workshop notes and have no business sitting on your client's page. The one exception is .well-known/, which is published because that's where domain verifications and indexing keys live.
Mode 2 — App with a backend (_worker.js)
If the published root carries a _worker.js file, the site is an APP: that file runs as a worker on the YaDominios Cloud server and receives ALL requests. It must be a single, already-compiled ES-module JavaScript file (bundle your dependencies with esbuild/rollup) with this shape:
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
if (url.pathname.startsWith("/media/")) {
// your backend here (env.DB, env.BUCKET)
return Response.json({ ok: true });
}
return env.ASSETS.fetch(request); // everything else: your static files
}
};
⚠️ Don't use /api/ for your backend routes. Static files are served before your worker; in apps that ship assets (Next.js/OpenNext) the /api/* prefix can be captured by the file router and return 404 without reaching your code. Use another prefix (/media, /upload, /data…). It's a real platform detail, verified in production.
Node.js: you don't need to configure anything to use Node APIs (node:stream, node:crypto, etc.). Every app with a backend runs with Node compat enabled automatically by YaDominios Cloud, with a recent compatibility date. That's why frameworks (Next.js with OpenNext, etc.) work with no tweaks.
When you publish an app, we automatically provision (idempotent, zero setup):
| Binding | What it is | How to use it |
|---|---|---|
env.DB | The site's own SQL database (serverless SQLite engine). Created as site-<name>-db. | await env.DB.prepare("SELECT * FROM t WHERE id=?").bind(1).all() |
env.BUCKET | The site's own file and image storage (bucket site-<name>). | await env.BUCKET.put("photo.jpg", bytes) · await env.BUCKET.get("photo.jpg") |
env.ASSETS | Your static files from the repo. | return env.ASSETS.fetch(request) |
| your variables | The environment variables you saved in the dashboard (secrets included). | env.STRIPE_KEY, env.API_URL… |
Note on env.BUCKET: the binding and permissions are already on the platform. If R2 isn't enabled on the account at publish time, the app ships anyway (without storage) and the bucket connects automatically on the next deploy once it's enabled. Program against env.BUCKET as usual.
Tables: schema.sql
If the published root carries schema.sql, we run it against the site's database on every publish. Write idempotent DDL:
CREATE TABLE IF NOT EXISTS customers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
_worker.js and schema.sql aren't served to the public: they're code/config.
The three project types
Before publishing, figure out which one you have. Everything else depends on this.
| Type | What the repo must contain | Needs a build? |
|---|---|---|
| a) Static site | index.html at the root or inside dist/, build/, out/, public/, site/, _site/ or docs/ | No |
| b) Pre-built app | A _worker.js at the root | No (you already built it) |
| c) Project that needs building Next.js, Astro, Vite, Nuxt… | The build output, on the yapanel-build branch | Yes |
⚠️ Case (c) is where everyone gets stuck: you cannot push source code and expect it to work. YaDominios Cloud does not run your build: it publishes whatever is on the connected branch. Push the source of a Next.js app and nothing comes out.
Projects that need building
The fix is a GitHub Action that builds on its own and leaves the output on a separate branch called yapanel-build. In the dashboard you connect that branch, not main.
Next.js: the size trap
Raw compiler output cannot be deployed. When you build a Next.js app with OpenNext, the .open-next folder ends up with over 1,000 files and around 19 MB. That does not go up as-is.
It has to be bundled into a single file before publishing. The GitHub Action we provide already does this with wrangler. If you build by hand, you cannot skip that bundling step: without it, the deploy fails.
The official adapter is OpenNext (@opennextjs/cloudflare). Here is the full Action:
# .github/workflows/build.yml
name: build-for-yadominios-cloud
on: { push: { branches: [main] } }
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npm ci && npx opennextjs-cloudflare build
# IMPORTANT: .open-next/worker.js is NOT standalone (it imports ./cloudflare/,
# ./middleware/, etc.). It must be bundled into ONE file.
# WRANGLER does the bundling, not raw esbuild: wrangler applies the correct
# Workers runtime rules (node:*, workerd conditions). A hand-rolled esbuild
# call either fails to compile or crashes on startup.
# --dry-run touches NO Cloudflare account: it only writes the file.
- run: |
npx wrangler deploy --dry-run --outdir=.dist-worker --minify
mkdir out-deploy
cp .dist-worker/worker.js out-deploy/_worker.js
cp -r .open-next/assets/* out-deploy/ 2>/dev/null || true
cp yadominios.json out-deploy/ 2>/dev/null || true
- uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_branch: yapanel-build
publish_dir: ./out-deploy
The wrangler step needs two things in your repo: wrangler as a devDependency, and a wrangler.jsonc at the root with at least this (the name doesn't matter — with --dry-run nothing is ever deployed to Cloudflare):
// wrangler.jsonc
{
"name": "my-site",
"main": ".open-next/worker.js",
"compatibility_date": "2026-07-01",
"compatibility_flags": ["nodejs_compat"],
"assets": { "directory": ".open-next/assets" }
}
Next.js with OpenNext: you don't have to do anything
OpenNext adds three Durable Object classes on its own — DOQueueHandler, DOShardedTagCache and BucketCachePurge — even if your app never uses them. YaDominios Cloud doesn't offer Durable Objects yet, and for a while that kept perfectly good packages from publishing.
Not anymore: we strip them at publish time, and we tell you so in the "Package checked" step of the log. You don't need to touch your config or write your own cleaner.
If you'd rather avoid them at the source, in your open-next.config.ts:
// open-next.config.ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
export default defineCloudflareConfig({
incrementalCache: undefined,
queue: undefined,
tagCache: undefined,
});
An honest warning: depending on the OpenNext version, that config does not always keep them out of the bundle — we found this out in a real integration. That's why we do the cleaning ourselves instead of trusting your config. To check it yourself:
grep -oE "DOQueueHandler|DOShardedTagCache|BucketCachePurge" .open-next/worker.js | sort -u
(The -E matters: without it the pipe is a literal character and the command reports "clean" every time, even with all three classes inside.)
What you lose without them: background revalidation (ISR). Pages render on request and are served from the edge cache — plenty for a store or a company site.
If your app uses its OWN Durable Objects (chat rooms, live counters, real time), that we do reject, and we say so: we can't strip those without breaking your app. They aren't available yet.
Other app-mode limits: no queues either. The final worker must be a single ES-module file whose export default has fetch; if your bundler emits CommonJS (module.exports), set the output format to esm. The normal size of a bundled Next.js app (3–6 MB) is not a problem.
If something fails, the panel tells you which step
Publishing is six steps and you watch them one by one, LIVE, on the site screen: repository found → branch downloaded → package checked → database and storage ready → site published → verified live. The last step is the guarantee: the platform VISITS your freshly published page and only marks green if it actually answers — "published" never means "stored but unreachable". If something breaks, the failing step turns red with the exact reason, and the later ones stay gray — they didn't fail, they never got to run.
Publish and republish
- First time: dashboard → yapanel.yadominios.com/panel/cloud → site name + repo URL + branch → “Publish my site.” Name rules: lowercase, numbers and hyphens, max 63, no
--and no reserved names (www, api, admin, docs…). - Your site goes live at
<name>.sitios.dev, with SSL, free, forever. - After that: every
git pushto the connected branch republishes the site on its own and clears the cache, so the change shows up immediately.
Only the connected branch republishes (and that's on purpose)
Pushes to other branches — say main on a project that needs building — do not republish. This isn't an oversight: the build takes a couple of minutes, so if main republished, it would deploy the previous build (the one already sitting there), not the change you just made. You'd see your change as “published” while actually looking at the old one.
That's why, on projects that need building, the connected branch is yapanel-build — the one the Action writes only after the build finishes.
If publishing fails
The reason is written in the dashboard: your site's card → “Error log.” If an AI is helping you, copy that text verbatim and paste it: it carries the path, the method, and the full stack trace.
And if a version can't be published, we say so. We retry it for you a few times — a passing hiccup on GitHub clears itself — but we don't spin in place: after the third attempt we stop and write it in your site's log, in red. Your site does not go down in the meantime: it stays online on the last version that published successfully. We get the alert at the same moment you do.
Publishing and connecting a domain are two separate things
You don't need a domain to publish. The natural order is:
- You publish → your page is already live at
<name>.sitios.devand you can share it. - Then, if you want, you connect your own domain from the site's card.
They're independent flows. The .sitios.dev subdomain never goes away, even after you connect your own domain.
Database API (console and migrations)
You can query your site's database over HTTP, without opening the dashboard. This is what lets an AI or a script create tables, seed data, and run queries.
1. Get your database token
Dashboard → YaDominios Cloud → your site's card → “View token” button.
It is shown only ONCE. Copy it and store it somewhere safe. If you lose it, generate a new one from the same button: the previous token stops working immediately.
The token opens only THAT site's database. It grants no access to any other site or to your account.
2. Make the request
POST https://yapanel.yadominios.com/api/hosting/db/query
Content-Type: application/json
{
"sitio": "your-site-name",
"token": "<your database token>",
"sql": "select * from orders where id = ?",
"params": [1]
}
Note: the field is spelled sitio (Spanish for “site”) — that is the exact key the API expects.
3. Response
{
"results": [ { "id": 1, "total": 250 } ],
"rowsRead": 1,
"rowsWritten": 0
}
Rules
- SQL is always parameterized: put
?placeholders inside"sql"and the values in"params", in the same order. Never concatenate values into the SQL string — that is how injections happen. - If your query takes no values, send
"params": []. - For migrations, send your
CREATE TABLE/ALTER TABLEin"sql".
Errors
| Code | What it means |
|---|---|
401 | The token is not valid (wrong token, or you regenerated it and are still using the old one). |
400 | A body field is missing, or the SQL itself failed. |
Full example (JavaScript)
const r = await fetch("https://yapanel.yadominios.com/api/hosting/db/query", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
sitio: "my-store",
token: process.env.YADOMINIOS_DB_TOKEN,
sql: "insert into orders (customer, total) values (?, ?)",
params: ["Ana", 250]
})
});
const data = await r.json();
console.log(data.rowsWritten); // 1
Platform configuration (yadominios.json)
Always set compatibility_date next to nodejs_compat: that flag requires a date of 2024-09-23 or later, and without it the behavior is left to the platform's default. For variables, KV, Durable Objects, queues, cron, and compatibility flags, add a yadominios.json at the published root (we also accept wrangler.jsonc, the format AIs already generate). We read that file and provision/wire everything automatically on YaDominios Cloud, isolated per site.
{
"compatibility_date": "2026-07-01",
"compatibility_flags": ["nodejs_compat"],
"vars": { "API_URL": "https://api.yourservice.com" },
"kv_namespaces": [{ "binding": "CACHE" }],
"durable_objects": { "bindings": [{ "name": "ROOM", "class_name": "Room" }] },
"migrations": [{ "new_sqlite_classes": ["Room"] }],
"queues": { "producers": [{ "binding": "QUEUE", "queue": "jobs" }] },
"triggers": { "crons": ["*/10 * * * *"] }
}
| Capability | How you declare it | How you use it in your code |
|---|---|---|
| Public variables | vars in the file (they live in the repo) | env.API_URL |
| SECRET variables (API keys) | In the dashboard → your site → Environment variables (NOT in the repo) | env.STRIPE_KEY |
| KV (cache/key-value) | kv_namespaces | await env.CACHE.get("k") |
| Durable Objects (state, real time) — not available yet | durable_objects + migrations | Not provisioned yet: a package exporting Durable Object classes is rejected at publish time. |
| Queues | queues.producers (requires Queues enabled on the account) | await env.QUEUE.send(msg) |
| Cron | triggers.crons | Your worker responds to GET /__scheduled (we invoke it on your cron) |
| Compatibility flags | compatibility_flags | — |
Secret variables: never put them in the repo. They go in the dashboard (stored encrypted server-side and injected as secrets at deploy; the dashboard shows only the names, never the values). When you save them, the site is re-published to apply them.
Cron (scheduled tasks): declare the expression in triggers.crons and expose a GET /__scheduled route in your worker with your task. Our scheduler invokes it at the right minute (it arrives with the x-yad-cron header). With OpenNext/Next, add that route as one more endpoint in your worker.
Custom domain (yourbusiness.com)
Órbita plans and up include a custom domain. It's self-service from the dashboard: YaDominios Cloud → your site → “Connect my own domain.” You type your domain and the dashboard shows you 2 unique nameservers; you paste them at your registrar (each has its own copy box) and within minutes to hours your domain serves your site with automatic HTTPS. Your name.sitios.dev subdomain keeps working forever.
Error log (observability)
If your app throws an exception in production, the visitor sees a friendly error page and the error is recorded with its full stack under YaDominios Cloud → your site → “Error log” (last 20). If an AI runs your site, copy the error verbatim and paste it in: it has the path, the method, and the trace to fix it.
How to check your change actually landed (read this before calling a deploy broken)
Do not search for an exact phrase inside the HTML. Frameworks split text across elements, so a literal search fails even when the change IS published.
Real example: you want to confirm the text “Your spot among the 100” shipped. In the published HTML it arrives like this:
<p>Your spot among the <span>100</span></p>
A grep "Your spot among the 100" returns nothing, and you'd conclude the deploy failed. It didn't: the text is there, split across two elements.
What to do instead:
- Search for a short, continuous fragment that doesn't cross tags (
"spot among the"). - Or open it in a browser and look at it — that's the definitive check.
- Or compare the “last published” timestamp in the dashboard against the time of your push.
Checklist for an AI publishing here
- Identify the project type (static / pre-built
_worker.js/ needs building). If it needs building, do not try to push source code: it will not work. - Static? → make sure
index.htmlis at the root or insidedist/ build/ out/ public/ site/ _site/ docs/, and push. - Pre-built app with a backend? →
_worker.jsat the root (single bundle,export default { fetch }), useenv.DB/env.BUCKET/env.ASSETS, addschema.sqlif you need tables. - Needs building (Next.js, Astro, Vite…)? → add the GitHub Action, confirm the
yapanel-buildbranch was created, and connect that branch in the dashboard. On Next.js the bundling step is mandatory: raw.open-next(1,000+ files, ~19 MB) does not deploy. - Connect the repo at yapanel.yadominios.com/panel/cloud (once). Every push to the connected branch publishes on its own.
- Data? → get the database token via “View token” and use
POST https://yapanel.yadominios.com/api/hosting/db/querywith parameterized SQL. - When verifying, don't grep for literal phrases in the HTML (see above). If something failed, read “Error log” on the site's card.
Frequently asked questions
Do you run my npm run build?
No. We deploy what's in the repo: static HTML or an already-compiled _worker.js. The build is automated with a GitHub Action in your own repo.
Can I use plain Node.js (Express)?
Not directly: the backend runs as a worker (export default { fetch }). Express doesn't apply; use the worker pattern or a framework with an adapter (Next.js via OpenNext, Hono, etc.).
Does the repo have to be public?
Yes for now: we connect public GitHub repositories. The files are read via the GitHub API on publish.
What are the database limits?
It's a serverless SQLite database: ideal for small and medium apps. Each site has its own isolated database, with automatic backup.
How do I connect my own domain (mydomain.com)?
Órbita plans and up include a custom domain: it's self-service from the dashboard (your site → Connect my own domain). The name.sitios.dev subdomain stays available forever.