Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
# CLAUDE.md — Node.js Hosting
# AGENTS.md — Node.js Hosting

This project is built to deploy on Node.js Hosting, a managed Node.js hosting platform. Use this file as context when helping build, debug, or prepare this app for deployment.

## Platform Overview

Node.js Hosting is a managed Node.js PaaS that supports Node.js applications and static sites. Customers upload their project folder through the GoDaddy interface — no Docker, no CI/CD pipelines, no infrastructure config needed. The platform handles SSL, CDN, and server-side compute automatically.
Node.js Hosting is a managed Node.js PaaS that supports Node.js applications and static sites. Customers deploy by **uploading a zip** or **connecting a Git repository** in the GoDaddy interface — no Docker, no CI/CD pipelines, no infrastructure config needed. The platform handles SSL, CDN, and server-side compute automatically.

A local `.git` folder does **not** by itself mean the customer uses Git deploy (zip exports often include `.git`). To avoid confusing the customer, we shouldn't make any references to uploading a zip or pushing to a remote Git in messages to the customer unless the customer explicitly asks about that.

## Deployment Flow

1. Customer uploads their project folder via the Node.js Hosting UI
1. Customer provides the app via **zip upload** or **Git sync** in the Node.js Hosting UI
2. The platform installs dependencies and builds the app
3. The app is deployed to a private preview environment (requires GoDaddy auth to view)
4. Once ready, the customer can publish to production and connect a custom domain

**Zip upload:** zip the project root, exclude irrelevant files (`node_modules`, build caches, and `.git`), and include a lockfile when possible.

**Git sync:** connect the remote repository, commit the lockfile, do not commit `node_modules` or `.env`.

When guiding a customer to publish changes, describe **both** Git sync and zip upload unless they’ve confirmed which they use.

## Requirements

Audit the project against each item below. Apply the **minimum** changes needed; do not remove existing functionality.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Supports **npm**, **pnpm**, and **yarn**.
```
Or symlink / marketplace when listed.
2. Open your **app** folder in your editor of choice (not this repo).
3. Invoke **`@godaddy-nodejs-hosting`** and ask it to prepare the project for Node.js Hosting (build or adapt).
3. Invoke **`/godaddy-nodejs-hosting`** (Claude) or **`@godaddy-nodejs-hosting`** (Cursor) and ask it to prepare the project for Node.js Hosting (build or adapt).
4. Validate before upload (optional; requires Node.js):
```bash
npm run validate -- /path/to/your-app
Expand Down
2 changes: 1 addition & 1 deletion docs/CONTRIBUTING-SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ These files must stay aligned:

**One PR** should update all affected files when changing deploy rules.

Bump `contractVersion` in `contract.md` for breaking contract changes (current: **2** — requires `main`, always requires `build`).
Bump `contractVersion` in `contract.md` for breaking contract changes (current: **3** — requires `main`, always requires `build`, MySQL E009/E010).

## Adding a framework

Expand Down
32 changes: 25 additions & 7 deletions skills/godaddy-nodejs-hosting/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Rules: [contract.md](contract.md). Recipes: [examples.md](examples.md). Errors:
| New app / scaffold / greenfield | [New app workflow](#new-app-workflow) |
| Existing repo / deploy failed / make this work on hosting | [Adapting an existing app](#adapting-an-existing-app) |
| AI export (Lovable, Replit, Bolt, etc.) | [Adapting an existing app](#adapting-an-existing-app) + [AI export quick fixes](#ai-export-quick-fixes) |
| Uses MySQL / database on platform | [Managed MySQL](#managed-mysql) |

## Audience routing

Expand All @@ -29,7 +30,7 @@ Rules: [contract.md](contract.md). Recipes: [examples.md](examples.md). Errors:
1. Output the [pre-upload checklist](#pre-upload-checklist) first in plain language.
2. Follow [Adapting an existing app](#adapting-an-existing-app) (existing zip or export).
3. Prefer the [static SPA + Express](examples.md#vite-react-vue-spa) or [static only](examples.md#static-only) recipe when there is no server.
4. One small change at a time; explain what to zip and upload.
4. One small change at a time; explain the next hosting step in plain language (Git sync or zip upload in the UI).
5. Run the validator before saying the app is ready.

**Technical user - existing app**: follow [Adapting an existing app](#adapting-an-existing-app); use the matching framework recipe for `start`/`build` only; do not re-explaining basics.
Expand All @@ -46,6 +47,21 @@ Rules: [contract.md](contract.md). Recipes: [examples.md](examples.md). Errors:
- One app per upload; lockfile when possible
- Outbound HTTP/HTTPS only; platform MySQL via `DB_*` env + `mysql2` if using a database ([contract.md](contract.md))

## Publishing updates (customer communication)

The platform supports **Git repository sync** and **zip upload** in the Node.js Hosting UI. See [AGENTS.md](../../AGENTS.md) Deployment Flow for agent context.

- **Do not infer** deploy method from `.git`, lockfiles, or export origin (a zip export may include `.git` from local development).
- **Default when telling the user how to publish:** use neutral wording — e.g. *“When you’re ready, update the app in Node.js Hosting using Git sync or a zip upload in the hosting UI.”*
- **Only go path-specific** (zip steps vs connect/push Git) if the user **states or asks** which they use.
- **Avoid** prescriptive one-path phrases like “upload a new zip” or “push to your remote” without that confirmation.

## Managed MySQL

Optional platform MySQL injects `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`. The app must read each from `process.env` (not hard-coded). Use **`mysql2`** in `dependencies` and parameterized queries.

Recipe: [examples.md#managed-mysql](examples.md#managed-mysql). Validator: **E009**, **E010** when a DB client or Prisma MySQL provider is detected.

## Adapting an existing app

Use this path when the user already has a project (local repo, zip export, or app that failed on Node.js Hosting). Goal: **hosting compatibility only** — not new features, refactors, or framework changes.
Expand Down Expand Up @@ -93,7 +109,9 @@ Symptom detail: [troubleshooting.md](troubleshooting.md).
| E008 | Add `main` pointing at an existing entry file |
| W001 | Remove `.env` from upload; use hosting UI for env vars |
| W002 | Optional: add `engines.node` |
| W004 | Remove `node_modules` from upload zip |
| W004 | Exclude `node_modules` from deployment (zip or Git) |
| E009 | Add `mysql2` to `dependencies` |
| E010 | Read each `DB_*` from `process.env` (see [managed-mysql](examples.md#managed-mysql)) |

**Special cases**

Expand All @@ -103,7 +121,7 @@ Symptom detail: [troubleshooting.md](troubleshooting.md).

For Replit, Lovable, and Bolt patterns, see [AI export quick fixes](#ai-export-quick-fixes) after applying this workflow.

Adaptation is complete under the same [done criteria](#done-criteria) as a new app (validator `0`, checklist, zip the project root).
Adaptation is complete under the same [done criteria](#done-criteria) as a new app (validator `0`, checklist, ready to update in Node.js Hosting).

## New app workflow

Expand Down Expand Up @@ -150,11 +168,11 @@ For Lovable/Bolt static hosting, use the [vite-react-vue-spa](examples.md#vite-r
## Pre-upload checklist

- [ ] `package.json`: `name`, `version`, `main`, `build`, `start`
- [ ] Production deps in `dependencies`; no `node_modules` in zip
- [ ] Production deps in `dependencies`; do not deploy `node_modules` (exclude from zip or Git)
- [ ] `process.env.PORT` for listening; secrets via `process.env`
- [ ] Zip under 100 MB
- [ ] Deployment artifact under 100 MB (zip upload)
- [ ] Runs locally: install → build → start
- [ ] Lockfile in zip; validator exit 0; no `.env`
- [ ] Lockfile present (committed or included in upload); validator exit 0; no `.env`
- [ ] HTTP/HTTPS outbound only; DB uses `DB_*` + `mysql2` if applicable

## Done criteria
Expand All @@ -163,4 +181,4 @@ Task is complete only when:

1. `validate-paas.mjs` exits `0` on the project directory.
2. Pre-upload checklist is satisfied.
3. User knows to zip the **project root** (single app folder) for upload.
3. User knows they can publish/update the app in Node.js Hosting (Git sync or zip upload in the UI). Mention only the path they use if they’ve already said so.
8 changes: 5 additions & 3 deletions skills/godaddy-nodejs-hosting/contract.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Node.js Hosting deploy contract

contractVersion: 2
contractVersion: 3

Testable rules for apps deployed on GoDaddy Node.js Hosting (PaaS). Enforced by [scripts/validate-paas.mjs](scripts/validate-paas.mjs) where noted.

Expand All @@ -27,7 +27,7 @@ Testable rules for apps deployed on GoDaddy Node.js Hosting (PaaS). Enforced by
| C9 | Vite: platform updates allowed-hosts; dev/preview must respect `PORT`. | — |
| C10 | Upload zip under 100 MB; exclude `node_modules`, caches, large artifacts. | W004 |
| C11 | Outbound: HTTP (80), HTTPS (443), and GoDaddy managed MySQL only. | — |
| C12 | If using DB: read `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` from env; use `mysql2` and parameterized queries. External DBs on non-80/443 are not reachable. | |
| C12 | If using DB: read `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` from `process.env`; use `mysql2` in `dependencies` and parameterized queries. External DBs on non-80/443 are not reachable. | E009, E010 |

## Framework build hints

Expand Down Expand Up @@ -63,4 +63,6 @@ These must not appear only in `devDependencies` if required at runtime:
| W001 | `.env` file present in project |
| W002 | Missing `engines.node` |
| W003 | Could not verify start script target |
| W004 | `node_modules` present (exclude from zip) |
| W004 | `node_modules` present (exclude from deployment) |
| E009 | Database use without `mysql2` in `dependencies` |
| E010 | Missing `process.env` reference for a required `DB_*` variable |
35 changes: 35 additions & 0 deletions skills/godaddy-nodejs-hosting/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,38 @@ app.get('*', (req, res) => {
});
app.listen(port);
```

---

## managed-mysql

**Detection:** `mysql2`, `sequelize`, `knex`, `typeorm`, or `mariadb` in dependencies; or Prisma `provider = "mysql"` in `prisma/schema.prisma`.

**Platform:** Since each Node.js Hosting app gets its own MySQL capacity automatically, the platform sets `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, and `DB_PASSWORD`. Read **each** from `process.env` in application code (typically one config module).

**package.json:**

```json
{
"dependencies": {
"express": "^4.18.0",
"mysql2": "^3.11.0"
}
}
```

**db.js (all five vars from env):**

```javascript
const mysql = require('mysql2/promise');

module.exports = mysql.createPool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
});
```

Use **parameterized** queries (`pool.query('SELECT * FROM users WHERE id = ?', [id])`). Do not interpolate user input into SQL strings.
79 changes: 78 additions & 1 deletion skills/godaddy-nodejs-hosting/scripts/validate-paas.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ const SKIP_DIRS = new Set([
'coverage',
]);

const DB_ENV_VARS = ['DB_HOST', 'DB_PORT', 'DB_NAME', 'DB_USER', 'DB_PASSWORD'];

const MYSQL_CLIENT_PACKAGES = [
'mysql2',
'sequelize',
'knex',
'typeorm',
'mariadb',
];

const errors = [];
const warnings = [];

Expand Down Expand Up @@ -267,7 +277,73 @@ function checkPackageMetadata(pkg, projectRoot) {
function checkUploadHygiene(projectRoot) {
const nm = pathUnderProjectRoot(projectRoot, 'node_modules');
if (nm && existsSync(nm)) {
warn('W004', 'node_modules present — exclude from upload zip (platform runs install)');
warn(
'W004',
'node_modules present — exclude from deployment (zip or Git); platform runs install',
);
}
}

export function envVarReferencedInContent(content, varName) {
const patterns = [
new RegExp(`process\\.env\\.${varName}\\b`),
new RegExp(`process\\.env\\?\\.${varName}\\b`),
new RegExp(`process\\.env\\[\\s*['"]${varName}['"]\\s*\\]`),
];
if (patterns.some((re) => re.test(content))) return true;
return new RegExp(`\\b${varName}\\b[^=]*=\\s*process\\.env`).test(content);
}

function envVarReferencedInFiles(files, varName) {
for (const file of files) {
let content;
try {
content = readFileSync(file, 'utf8');
} catch {
continue;
}
if (envVarReferencedInContent(content, varName)) return true;
}
return false;
}

function usesMysqlFromPrisma(projectRoot) {
const schemaPath = pathUnderProjectRoot(projectRoot, 'prisma', 'schema.prisma');
if (!schemaPath || !existsSync(schemaPath)) return false;
try {
const schema = readFileSync(schemaPath, 'utf8');
return /provider\s*=\s*["']mysql["']/i.test(schema);
} catch {
return false;
}
}

export function usesDatabase(deps, projectRoot) {
if (MYSQL_CLIENT_PACKAGES.some((name) => deps[name])) return true;
return usesMysqlFromPrisma(projectRoot);
}

function checkDatabaseConfig(projectRoot, pkg) {
const deps = allDeps(pkg);
if (!usesDatabase(deps, projectRoot)) return;

const runtimeDeps = pkg.dependencies || {};
if (!runtimeDeps.mysql2) {
if (deps.mysql2) {
err('E009', 'mysql2 is only in devDependencies — move to dependencies for production');
} else {
err('E009', 'Database client detected — add mysql2 to dependencies for platform MySQL');
}
}

const files = walkSourceFiles(projectRoot);
for (const varName of DB_ENV_VARS) {
if (!envVarReferencedInFiles(files, varName)) {
err(
'E010',
`Managed MySQL requires process.env.${varName} (set by platform when DB is enabled)`,
);
}
}
}

Expand Down Expand Up @@ -353,6 +429,7 @@ function validateProject(projectRoot) {
checkBuildScript(pkg, deps);
checkRuntimeDeps(pkg);
checkHardcodedPorts(projectRoot);
checkDatabaseConfig(projectRoot, pkg);

const envPath = pathUnderProjectRoot(projectRoot, '.env');
if (envPath && existsSync(envPath)) {
Expand Down
2 changes: 1 addition & 1 deletion skills/godaddy-nodejs-hosting/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Map symptoms to [contract.md](contract.md) rules and validator IDs.
| Secrets leaked | C8 | W001 | Remove `.env` from upload; use hosting UI for env vars |
| Monorepo upload fails | C1 | — | Upload single package folder only |
| External API unreachable | C11 | — | Use HTTP/HTTPS (80/443) only |
| Database connection fails | C12 | | Use platform MySQL `DB_*` env vars and `mysql2`; not remote :3306 |
| Database connection fails | C12 | E009, E010 | Use platform MySQL: all five `DB_*` from `process.env`, `mysql2` in `dependencies`; not remote :3306 |

## Validator exit codes

Expand Down
2 changes: 1 addition & 1 deletion tests/drift-check.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const REQUIRED_IN_AGENTS = [
];

const REQUIRED_IN_CONTRACT = [
'contractVersion: 2',
'contractVersion: 3',
'process.env.PORT',
'scripts.start',
'scripts.build',
Expand Down
13 changes: 13 additions & 0 deletions tests/fixtures/bad-mysql-missing-env/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "bad-mysql-missing-env",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"build": "echo build",
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.0",
"mysql2": "^3.11.0"
}
}
20 changes: 20 additions & 0 deletions tests/fixtures/bad-mysql-missing-env/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const express = require('express');
const mysql = require('mysql2/promise');

const app = express();
const port = process.env.PORT || 3000;

const pool = mysql.createPool({
host: process.env.DB_HOST,
port: 3306,
database: 'app',
user: 'root',
password: 'secret',
});

app.get('/', async (req, res) => {
const [rows] = await pool.query('SELECT 1 AS ok');
res.json(rows[0]);
});

app.listen(port, () => console.log(`Listening on ${port}`));
13 changes: 13 additions & 0 deletions tests/fixtures/mysql-good/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "mysql-good",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"build": "echo build",
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.0",
"mysql2": "^3.11.0"
}
}
20 changes: 20 additions & 0 deletions tests/fixtures/mysql-good/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const express = require('express');
const mysql = require('mysql2/promise');

const app = express();
const port = process.env.PORT || 3000;

const pool = mysql.createPool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
});

app.get('/', async (req, res) => {
const [rows] = await pool.query('SELECT 1 AS ok');
res.json(rows[0]);
});

app.listen(port, () => console.log(`Listening on ${port}`));
Loading