nerdsystem 0.0.22 → 0.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.d.ts CHANGED
@@ -53,6 +53,8 @@ export interface Project {
53
53
  name: string;
54
54
  scriptName: string;
55
55
  url: string;
56
+ /** The address on the owner's own domain, once one is verified. Absent from an older platform. */
57
+ customUrl?: string | null;
56
58
  }
57
59
  export interface Deployment {
58
60
  id: string;
package/dist/cli.js CHANGED
@@ -67,7 +67,7 @@ const HELP = `nerd: start, check, and deploy a web app on nerdsystem
67
67
  nerd billing portal Manage cards, invoices, and plan
68
68
  nerd members [invite <email> --role r | remove <userId>] Org members and invitations
69
69
  nerd clients [create <slug> --name n | release <slug>] Studio: one org per client, billed through yours
70
- nerd domains [add <hostname> | remove <id>] Custom domains for this project
70
+ nerd domains [add <domain> | remove <id>] The app on your own domain, with the DNS steps for it
71
71
  nerd logs [--since 30m] [--limit 100] [--q text] [--pr n] Logs from the deployed app
72
72
  nerd audit [--out file.csv] Export deployments and builds (Studio)
73
73
 
@@ -46,6 +46,8 @@ export declare function checksMetadata(results: GateResult[]): {
46
46
  export interface ShipResult {
47
47
  deployment: Deployment;
48
48
  url: string;
49
+ /** The app's address on the owner's own domain, once one is verified; null otherwise. */
50
+ customUrl: string | null;
49
51
  migrations: {
50
52
  applied: string[];
51
53
  skipped: string[];
@@ -185,11 +185,14 @@ export async function shipBuild(args) {
185
185
  }
186
186
  const { deployment } = await api.post(projectPath(nerd, "/deployments"), form);
187
187
  const { project } = await api.get(projectPath(nerd));
188
- log.ok(`Deployed ${deployment.id} (${deployment.status}) → ${project.url}`);
188
+ log.ok(`Deployed ${deployment.id} (${deployment.status}) → ${project.customUrl ?? project.url}`);
189
+ // The owner knows the app by their own domain; the platform address is the one that is probed, because it always resolves.
190
+ if (project.customUrl)
191
+ log.info(`Also at ${project.url}`);
189
192
  if (!opts["skip-probe"] && deployment.status === "live")
190
193
  log.step(`Probing ${project.url}`);
191
194
  const probes = await probeAfterDeploy(root, deployment, project.url, Boolean(opts["skip-probe"]));
192
- return { deployment, url: project.url, migrations: mig, probes, source, sourceSkipped, lineage, warnings: pack.warnings };
195
+ return { deployment, url: project.url, customUrl: project.customUrl ?? null, migrations: mig, probes, source, sourceSkipped, lineage, warnings: pack.warnings };
193
196
  }
194
197
  /** The one line about the code after a live deploy, keyed on what the API recorded, not on what we sent. */
195
198
  export function sourceLine(r) {
@@ -230,7 +233,7 @@ export async function deploy(opts) {
230
233
  const wantReview = r.deployment.status === "live" && Boolean(r.source);
231
234
  if (opts.json) {
232
235
  const review = wantReview ? await platformReview(api, `${projectPath(nerd)}/sources/${r.source.id}`, reviewPage) : null;
233
- return printJson({ deployment: r.deployment, url: r.url, migrations: r.migrations, probes: r.probes, source: r.source, sourceSkipped: r.sourceSkipped, lineage: r.lineage.passed, warnings: r.warnings, review });
236
+ return printJson({ deployment: r.deployment, url: r.url, customUrl: r.customUrl, migrations: r.migrations, probes: r.probes, source: r.source, sourceSkipped: r.sourceSkipped, lineage: r.lineage.passed, warnings: r.warnings, review });
234
237
  }
235
238
  printProbe(r.url, r.probes);
236
239
  if (r.deployment.status === "live") {
@@ -243,7 +246,7 @@ export async function deploy(opts) {
243
246
  log.step("Your app is live. Asking for a model review of how it is put together (up to a minute or two; safe to stop waiting)");
244
247
  printReview(await platformReview(api, `${projectPath(nerd)}/sources/${r.source.id}`, reviewPage));
245
248
  }
246
- log.info(`First time? Sign up on your app at ${r.url}, then make yourself admin in the dashboard's Auth tab: ${dashboardUrl(api.apiUrl)}/app/${nerd.org}/projects/${nerd.project}/auth`);
249
+ log.info(`First time? Sign up on your app at ${r.customUrl ?? r.url}, then make yourself admin in the dashboard's Auth tab: ${dashboardUrl(api.apiUrl)}/app/${nerd.org}/projects/${nerd.project}/auth`);
247
250
  }
248
251
  export async function deployments(opts) {
249
252
  const nerd = readNerdJson(findProjectRoot());
@@ -192,4 +192,4 @@ export async function init(dir, args) {
192
192
  log.ok(`Ready. Next:\n cd ${dir}\n pnpm install\n nerd dev\n\nThen open the folder in your coding agent and ask it to build the first feature; the nerdsystem skill walks it through.\nDeploys go to ${project.url}`);
193
193
  }
194
194
  // Kept in sync with packages/sdk/package.json by the release script.
195
- const SDK_VERSION = "0.0.22";
195
+ const SDK_VERSION = "0.0.24";
@@ -11,6 +11,23 @@ export declare function membersRemove(userId: string | undefined, opts: {
11
11
  json: boolean;
12
12
  org?: string;
13
13
  }): Promise<void>;
14
+ /** The platform's steps for the company that runs the domain's DNS. Absent from an older platform. */
15
+ interface DomainSetup {
16
+ address: string;
17
+ forwards: string | null;
18
+ dnsHost: {
19
+ id: string;
20
+ name: string;
21
+ };
22
+ method: string;
23
+ steps: string[];
24
+ after: string;
25
+ }
26
+ /**
27
+ * The steps as the platform wrote them, numbered, with a line telling an agent how to pass them on. The person
28
+ * who owns the domain is usually not technical: they get these steps and nothing to choose between.
29
+ */
30
+ export declare function setupText(s: DomainSetup): string;
14
31
  export declare function domainsList(opts: {
15
32
  json: boolean;
16
33
  }): Promise<void>;
@@ -45,3 +62,4 @@ export declare function clientsRelease(slug: string | undefined, opts: {
45
62
  json: boolean;
46
63
  org?: string;
47
64
  }): Promise<void>;
65
+ export {};
@@ -35,23 +35,56 @@ export async function membersRemove(userId, opts) {
35
35
  return printJson(r);
36
36
  log.ok("Removed.");
37
37
  }
38
+ /**
39
+ * The steps as the platform wrote them, numbered, with a line telling an agent how to pass them on. The person
40
+ * who owns the domain is usually not technical: they get these steps and nothing to choose between.
41
+ */
42
+ export function setupText(s) {
43
+ const known = s.dnsHost.id !== "unknown";
44
+ return [
45
+ `The app's address will be https://${s.address}${s.forwards ? `, and ${s.forwards} forwards to it` : ""}.`,
46
+ known ? `This domain's DNS is run by ${s.dnsHost.name}${s.method === "move-dns" ? ", which cannot point a bare domain name at another service, so the DNS moves to Cloudflare (free) first" : ""}. The steps for the domain's owner:` : "The steps for the domain's owner:",
47
+ "",
48
+ ...s.steps.map((step, i) => ` ${i + 1}. ${step}`),
49
+ "",
50
+ s.after,
51
+ "",
52
+ "If you are a coding agent: give the owner these steps as they are, numbered, and nothing else to decide. Do not ask which company they use (it was looked up) and do not offer other record types. When they say it is done, run `nerd domains` until the status is active, then open the address to confirm.",
53
+ ].join("\n");
54
+ }
55
+ /** Whether the domain without www forwards to the app yet, in words an agent can pass on. */
56
+ function bareText(b) {
57
+ if (!b)
58
+ return "";
59
+ if (b.status === "forwarding")
60
+ return `${b.hostname} forwards here`;
61
+ if (b.status === "starting")
62
+ return `${b.hostname} is starting (certificate, under a minute)`;
63
+ return `${b.hostname} does not point here yet`;
64
+ }
38
65
  export async function domainsList(opts) {
39
66
  const nerd = readNerdJson(findProjectRoot());
40
67
  const r = await ApiClient.fromConfig().get(projectPath(nerd, "/domains"));
41
68
  if (opts.json)
42
69
  return printJson(r);
43
70
  if (!r.domains.length)
44
- return log.info(`No custom domains. Add one: nerd domains add app.example.com`);
45
- table(r.domains.map((d) => ({ hostname: d.hostname, status: d.status, ssl: d.sslStatus ?? "", error: d.lastError ?? "", id: d.id })), ["hostname", "status", "ssl", "error", "id"]);
46
- log.info(`\nPoint each hostname at ${r.cnameTarget} with a CNAME record.`);
71
+ return log.info(`No custom domains. Add one: nerd domains add example.com`);
72
+ table(r.domains.map((d) => ({ hostname: d.hostname, status: d.status, ssl: d.sslStatus ?? "", does: d.redirectTo ? `forwards to ${d.redirectTo}` : "the app", "bare name": bareText(d.bare), error: d.lastError ?? "", id: d.id })), ["hostname", "status", "ssl", "does", "bare name", "error", "id"]);
73
+ // Only addresses still waiting for their DNS records have steps left to do.
74
+ for (const s of r.setups ?? [])
75
+ log.info(`\n${setupText(s)}`);
76
+ if (!r.setups && r.domains.some((d) => d.status !== "active"))
77
+ log.info(`\nPoint each hostname at ${r.cnameTarget} with a CNAME record.`);
47
78
  }
48
79
  export async function domainsAdd(hostname, opts) {
49
80
  if (!hostname)
50
- throw new CliError("Usage: nerd domains add app.example.com");
81
+ throw new CliError("Usage: nerd domains add example.com");
51
82
  const nerd = readNerdJson(findProjectRoot());
52
83
  const r = await ApiClient.fromConfig().post(projectPath(nerd, "/domains"), { hostname });
53
84
  if (opts.json)
54
85
  return printJson(r);
86
+ if (r.setup)
87
+ return log.ok(`Added ${r.domain.hostname}.\n\n${setupText(r.setup)}`);
55
88
  log.ok(`Added ${r.domain.hostname}. Create this DNS record, then check with \`nerd domains\`:\n\n ${r.domain.hostname} CNAME ${r.cnameTarget}`);
56
89
  }
57
90
  export async function domainsRemove(idOrHostname, opts) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nerdsystem",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "description": "nerd: start, check, and deploy a web app on nerdsystem from the terminal or a coding agent.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -178,6 +178,17 @@ After a deploy is live, `nerd deploy` asks the platform for a model review of th
178
178
  - **Secrets** (Stripe keys, API tokens): never in code or in the client. Locally they live in `.dev.vars` (gitignored). For production, ask the person to add it under the app's Settings tab in the dashboard, or to run `nerd secret set NAME` and paste the value; it reaches the app as `c.env.NAME` on the next deploy. Pull request previews do not get it unless it was set with `--previews` (use that only for keys that are safe in a preview, such as a test-mode key). `nerd secret list` shows names only. Do not put a real secret value in any command you run yourself.
179
179
  - **Email**: `sendEmail(c.env, { to, subject, text, html?, replyTo? })` from `@nerdsystem/sdk` sends through the platform. The relay only sends to addresses that have an account on this app (someone who signed up); any other recipient is refused with a 400, and the sender name is always the app's name. On the Free plan the recipient must also have confirmed their address by finishing a password reset (403 otherwise); paid plans can write to every signed-up user. It is limited per day by plan (50 on Free, 1,000 on Pro at $39 a month, 2,000 on Studio at $99 a month), so use it for transactional mail such as receipts and notifications, not bulk. Plans can be paid monthly or yearly at ten times the monthly price. Pass `replyTo` (the owner's address, or one they give you) whenever a person might answer the message; without it replies go to a mailbox nobody reads. Mail goes out under the app's name from the platform's address; on Pro and Studio the owner can switch it to their own domain in the dashboard under Settings, which needs no code change.
180
180
 
181
+ ## Putting the app on the owner's own domain
182
+
183
+ Paid plans only; on Free the command answers with the plan that includes it, so say that and stop.
184
+
185
+ 1. Run `nerd domains add example.com` with the bare domain the owner gives you. The platform makes `www.example.com` the app's address and sets the bare name up to forward to it. It also looks up which company runs the domain's DNS and prints the exact steps for that company.
186
+ 2. Give the owner those steps as printed, numbered. Do not ask who their registrar is, do not show a table of record types, and do not offer alternatives: the platform already chose. The steps are things only they can do (signing in where the domain is managed).
187
+ 3. When they say it is done, run `nerd domains` every minute or two until the status is `active` and the "bare name" column says it forwards here, then open both addresses and confirm they load with https. If it is still pending after an hour, run `nerd domains` and read the steps again with them; the usual cause is a leftover parking record.
188
+ 4. Tell them two things once it is live: they sign in once more on the new address (same account, same data), and links to the old `.nerdsystem.app` address keep working.
189
+
190
+ For a subdomain such as `app.example.com`, the same command gives a single record to add.
191
+
181
192
  ## Debugging production
182
193
 
183
194
  When something is wrong on the deployed app: `nerd logs --since 1h` (add `--q "text"` to search, `--pr 12` for a preview) shows errors with the request that caused them. Reproduce locally with `nerd dev` using the same input, fix it, run `nerd verify`, and deploy or open a pull request. If a deploy made things worse, `nerd rollback` puts the previous production bundle back without touching the database. Write a one-line note on every deploy (`nerd deploy --note "fix: todos query filtered by user"`); the dashboard shows these as a changelog people can read.
@@ -2,7 +2,7 @@
2
2
  description: How to build, run, and deploy this app on nerdsystem. Applies to server, database, auth, and deploy work.
3
3
  alwaysApply: true
4
4
  ---
5
- <!-- generated from .claude/skills/nerdsystem/SKILL.md (7bfa9a6cabd1); edit the skill, then run: nerd agents sync -->
5
+ <!-- generated from .claude/skills/nerdsystem/SKILL.md (e9b33e090df6); edit the skill, then run: nerd agents sync -->
6
6
 
7
7
  # Building on nerdsystem
8
8
 
@@ -179,6 +179,17 @@ After a deploy is live, `nerd deploy` asks the platform for a model review of th
179
179
  - **Secrets** (Stripe keys, API tokens): never in code or in the client. Locally they live in `.dev.vars` (gitignored). For production, ask the person to add it under the app's Settings tab in the dashboard, or to run `nerd secret set NAME` and paste the value; it reaches the app as `c.env.NAME` on the next deploy. Pull request previews do not get it unless it was set with `--previews` (use that only for keys that are safe in a preview, such as a test-mode key). `nerd secret list` shows names only. Do not put a real secret value in any command you run yourself.
180
180
  - **Email**: `sendEmail(c.env, { to, subject, text, html?, replyTo? })` from `@nerdsystem/sdk` sends through the platform. The relay only sends to addresses that have an account on this app (someone who signed up); any other recipient is refused with a 400, and the sender name is always the app's name. On the Free plan the recipient must also have confirmed their address by finishing a password reset (403 otherwise); paid plans can write to every signed-up user. It is limited per day by plan (50 on Free, 1,000 on Pro at $39 a month, 2,000 on Studio at $99 a month), so use it for transactional mail such as receipts and notifications, not bulk. Plans can be paid monthly or yearly at ten times the monthly price. Pass `replyTo` (the owner's address, or one they give you) whenever a person might answer the message; without it replies go to a mailbox nobody reads. Mail goes out under the app's name from the platform's address; on Pro and Studio the owner can switch it to their own domain in the dashboard under Settings, which needs no code change.
181
181
 
182
+ ## Putting the app on the owner's own domain
183
+
184
+ Paid plans only; on Free the command answers with the plan that includes it, so say that and stop.
185
+
186
+ 1. Run `nerd domains add example.com` with the bare domain the owner gives you. The platform makes `www.example.com` the app's address and sets the bare name up to forward to it. It also looks up which company runs the domain's DNS and prints the exact steps for that company.
187
+ 2. Give the owner those steps as printed, numbered. Do not ask who their registrar is, do not show a table of record types, and do not offer alternatives: the platform already chose. The steps are things only they can do (signing in where the domain is managed).
188
+ 3. When they say it is done, run `nerd domains` every minute or two until the status is `active` and the "bare name" column says it forwards here, then open both addresses and confirm they load with https. If it is still pending after an hour, run `nerd domains` and read the steps again with them; the usual cause is a leftover parking record.
189
+ 4. Tell them two things once it is live: they sign in once more on the new address (same account, same data), and links to the old `.nerdsystem.app` address keep working.
190
+
191
+ For a subdomain such as `app.example.com`, the same command gives a single record to add.
192
+
182
193
  ## Debugging production
183
194
 
184
195
  When something is wrong on the deployed app: `nerd logs --since 1h` (add `--q "text"` to search, `--pr 12` for a preview) shows errors with the request that caused them. Reproduce locally with `nerd dev` using the same input, fix it, run `nerd verify`, and deploy or open a pull request. If a deploy made things worse, `nerd rollback` puts the previous production bundle back without touching the database. Write a one-line note on every deploy (`nerd deploy --note "fix: todos query filtered by user"`); the dashboard shows these as a changelog people can read.
@@ -1,4 +1,4 @@
1
- <!-- generated from .claude/skills/nerdsystem/SKILL.md (7bfa9a6cabd1); edit the skill, then run: nerd agents sync -->
1
+ <!-- generated from .claude/skills/nerdsystem/SKILL.md (e9b33e090df6); edit the skill, then run: nerd agents sync -->
2
2
 
3
3
  # Building on nerdsystem
4
4
 
@@ -175,6 +175,17 @@ After a deploy is live, `nerd deploy` asks the platform for a model review of th
175
175
  - **Secrets** (Stripe keys, API tokens): never in code or in the client. Locally they live in `.dev.vars` (gitignored). For production, ask the person to add it under the app's Settings tab in the dashboard, or to run `nerd secret set NAME` and paste the value; it reaches the app as `c.env.NAME` on the next deploy. Pull request previews do not get it unless it was set with `--previews` (use that only for keys that are safe in a preview, such as a test-mode key). `nerd secret list` shows names only. Do not put a real secret value in any command you run yourself.
176
176
  - **Email**: `sendEmail(c.env, { to, subject, text, html?, replyTo? })` from `@nerdsystem/sdk` sends through the platform. The relay only sends to addresses that have an account on this app (someone who signed up); any other recipient is refused with a 400, and the sender name is always the app's name. On the Free plan the recipient must also have confirmed their address by finishing a password reset (403 otherwise); paid plans can write to every signed-up user. It is limited per day by plan (50 on Free, 1,000 on Pro at $39 a month, 2,000 on Studio at $99 a month), so use it for transactional mail such as receipts and notifications, not bulk. Plans can be paid monthly or yearly at ten times the monthly price. Pass `replyTo` (the owner's address, or one they give you) whenever a person might answer the message; without it replies go to a mailbox nobody reads. Mail goes out under the app's name from the platform's address; on Pro and Studio the owner can switch it to their own domain in the dashboard under Settings, which needs no code change.
177
177
 
178
+ ## Putting the app on the owner's own domain
179
+
180
+ Paid plans only; on Free the command answers with the plan that includes it, so say that and stop.
181
+
182
+ 1. Run `nerd domains add example.com` with the bare domain the owner gives you. The platform makes `www.example.com` the app's address and sets the bare name up to forward to it. It also looks up which company runs the domain's DNS and prints the exact steps for that company.
183
+ 2. Give the owner those steps as printed, numbered. Do not ask who their registrar is, do not show a table of record types, and do not offer alternatives: the platform already chose. The steps are things only they can do (signing in where the domain is managed).
184
+ 3. When they say it is done, run `nerd domains` every minute or two until the status is `active` and the "bare name" column says it forwards here, then open both addresses and confirm they load with https. If it is still pending after an hour, run `nerd domains` and read the steps again with them; the usual cause is a leftover parking record.
185
+ 4. Tell them two things once it is live: they sign in once more on the new address (same account, same data), and links to the old `.nerdsystem.app` address keep working.
186
+
187
+ For a subdomain such as `app.example.com`, the same command gives a single record to add.
188
+
178
189
  ## Debugging production
179
190
 
180
191
  When something is wrong on the deployed app: `nerd logs --since 1h` (add `--q "text"` to search, `--pr 12` for a preview) shows errors with the request that caused them. Reproduce locally with `nerd dev` using the same input, fix it, run `nerd verify`, and deploy or open a pull request. If a deploy made things worse, `nerd rollback` puts the previous production bundle back without touching the database. Write a one-line note on every deploy (`nerd deploy --note "fix: todos query filtered by user"`); the dashboard shows these as a changelog people can read.
@@ -1,4 +1,4 @@
1
- <!-- generated from .claude/skills/nerdsystem/SKILL.md (7bfa9a6cabd1); edit the skill, then run: nerd agents sync -->
1
+ <!-- generated from .claude/skills/nerdsystem/SKILL.md (e9b33e090df6); edit the skill, then run: nerd agents sync -->
2
2
 
3
3
  # Building on nerdsystem
4
4
 
@@ -175,6 +175,17 @@ After a deploy is live, `nerd deploy` asks the platform for a model review of th
175
175
  - **Secrets** (Stripe keys, API tokens): never in code or in the client. Locally they live in `.dev.vars` (gitignored). For production, ask the person to add it under the app's Settings tab in the dashboard, or to run `nerd secret set NAME` and paste the value; it reaches the app as `c.env.NAME` on the next deploy. Pull request previews do not get it unless it was set with `--previews` (use that only for keys that are safe in a preview, such as a test-mode key). `nerd secret list` shows names only. Do not put a real secret value in any command you run yourself.
176
176
  - **Email**: `sendEmail(c.env, { to, subject, text, html?, replyTo? })` from `@nerdsystem/sdk` sends through the platform. The relay only sends to addresses that have an account on this app (someone who signed up); any other recipient is refused with a 400, and the sender name is always the app's name. On the Free plan the recipient must also have confirmed their address by finishing a password reset (403 otherwise); paid plans can write to every signed-up user. It is limited per day by plan (50 on Free, 1,000 on Pro at $39 a month, 2,000 on Studio at $99 a month), so use it for transactional mail such as receipts and notifications, not bulk. Plans can be paid monthly or yearly at ten times the monthly price. Pass `replyTo` (the owner's address, or one they give you) whenever a person might answer the message; without it replies go to a mailbox nobody reads. Mail goes out under the app's name from the platform's address; on Pro and Studio the owner can switch it to their own domain in the dashboard under Settings, which needs no code change.
177
177
 
178
+ ## Putting the app on the owner's own domain
179
+
180
+ Paid plans only; on Free the command answers with the plan that includes it, so say that and stop.
181
+
182
+ 1. Run `nerd domains add example.com` with the bare domain the owner gives you. The platform makes `www.example.com` the app's address and sets the bare name up to forward to it. It also looks up which company runs the domain's DNS and prints the exact steps for that company.
183
+ 2. Give the owner those steps as printed, numbered. Do not ask who their registrar is, do not show a table of record types, and do not offer alternatives: the platform already chose. The steps are things only they can do (signing in where the domain is managed).
184
+ 3. When they say it is done, run `nerd domains` every minute or two until the status is `active` and the "bare name" column says it forwards here, then open both addresses and confirm they load with https. If it is still pending after an hour, run `nerd domains` and read the steps again with them; the usual cause is a leftover parking record.
185
+ 4. Tell them two things once it is live: they sign in once more on the new address (same account, same data), and links to the old `.nerdsystem.app` address keep working.
186
+
187
+ For a subdomain such as `app.example.com`, the same command gives a single record to add.
188
+
178
189
  ## Debugging production
179
190
 
180
191
  When something is wrong on the deployed app: `nerd logs --since 1h` (add `--q "text"` to search, `--pr 12` for a preview) shows errors with the request that caused them. Reproduce locally with `nerd dev` using the same input, fix it, run `nerd verify`, and deploy or open a pull request. If a deploy made things worse, `nerd rollback` puts the previous production bundle back without touching the database. Write a one-line note on every deploy (`nerd deploy --note "fix: todos query filtered by user"`); the dashboard shows these as a changelog people can read.
@@ -1,4 +1,4 @@
1
- <!-- generated from .claude/skills/nerdsystem/SKILL.md (7bfa9a6cabd1); edit the skill, then run: nerd agents sync -->
1
+ <!-- generated from .claude/skills/nerdsystem/SKILL.md (e9b33e090df6); edit the skill, then run: nerd agents sync -->
2
2
 
3
3
  # Building on nerdsystem
4
4
 
@@ -175,6 +175,17 @@ After a deploy is live, `nerd deploy` asks the platform for a model review of th
175
175
  - **Secrets** (Stripe keys, API tokens): never in code or in the client. Locally they live in `.dev.vars` (gitignored). For production, ask the person to add it under the app's Settings tab in the dashboard, or to run `nerd secret set NAME` and paste the value; it reaches the app as `c.env.NAME` on the next deploy. Pull request previews do not get it unless it was set with `--previews` (use that only for keys that are safe in a preview, such as a test-mode key). `nerd secret list` shows names only. Do not put a real secret value in any command you run yourself.
176
176
  - **Email**: `sendEmail(c.env, { to, subject, text, html?, replyTo? })` from `@nerdsystem/sdk` sends through the platform. The relay only sends to addresses that have an account on this app (someone who signed up); any other recipient is refused with a 400, and the sender name is always the app's name. On the Free plan the recipient must also have confirmed their address by finishing a password reset (403 otherwise); paid plans can write to every signed-up user. It is limited per day by plan (50 on Free, 1,000 on Pro at $39 a month, 2,000 on Studio at $99 a month), so use it for transactional mail such as receipts and notifications, not bulk. Plans can be paid monthly or yearly at ten times the monthly price. Pass `replyTo` (the owner's address, or one they give you) whenever a person might answer the message; without it replies go to a mailbox nobody reads. Mail goes out under the app's name from the platform's address; on Pro and Studio the owner can switch it to their own domain in the dashboard under Settings, which needs no code change.
177
177
 
178
+ ## Putting the app on the owner's own domain
179
+
180
+ Paid plans only; on Free the command answers with the plan that includes it, so say that and stop.
181
+
182
+ 1. Run `nerd domains add example.com` with the bare domain the owner gives you. The platform makes `www.example.com` the app's address and sets the bare name up to forward to it. It also looks up which company runs the domain's DNS and prints the exact steps for that company.
183
+ 2. Give the owner those steps as printed, numbered. Do not ask who their registrar is, do not show a table of record types, and do not offer alternatives: the platform already chose. The steps are things only they can do (signing in where the domain is managed).
184
+ 3. When they say it is done, run `nerd domains` every minute or two until the status is `active` and the "bare name" column says it forwards here, then open both addresses and confirm they load with https. If it is still pending after an hour, run `nerd domains` and read the steps again with them; the usual cause is a leftover parking record.
185
+ 4. Tell them two things once it is live: they sign in once more on the new address (same account, same data), and links to the old `.nerdsystem.app` address keep working.
186
+
187
+ For a subdomain such as `app.example.com`, the same command gives a single record to add.
188
+
178
189
  ## Debugging production
179
190
 
180
191
  When something is wrong on the deployed app: `nerd logs --since 1h` (add `--q "text"` to search, `--pr 12` for a preview) shows errors with the request that caused them. Reproduce locally with `nerd dev` using the same input, fix it, run `nerd verify`, and deploy or open a pull request. If a deploy made things worse, `nerd rollback` puts the previous production bundle back without touching the database. Write a one-line note on every deploy (`nerd deploy --note "fix: todos query filtered by user"`); the dashboard shows these as a changelog people can read.