create-softeneers-app 0.1.0 → 0.2.1

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.
Files changed (84) hide show
  1. package/README.html +56 -0
  2. package/README.md +16 -8
  3. package/dist/args.js +33 -4
  4. package/dist/args.js.map +1 -1
  5. package/dist/fragments.js +127 -0
  6. package/dist/fragments.js.map +1 -0
  7. package/dist/index.js +18 -7
  8. package/dist/index.js.map +1 -1
  9. package/dist/prompts.js +27 -1
  10. package/dist/prompts.js.map +1 -1
  11. package/dist/templates.js +33 -8
  12. package/dist/templates.js.map +1 -1
  13. package/package.json +2 -2
  14. package/templates/express-api/.env.example +13 -0
  15. package/templates/express-api/README.md +77 -0
  16. package/templates/express-api/docker-compose.yml +15 -0
  17. package/templates/express-api/package.json +36 -0
  18. package/templates/express-api/softeneers.template.json +17 -0
  19. package/templates/express-api/src/auth/auth.ts +11 -0
  20. package/templates/express-api/src/cars/demo.ts +10 -0
  21. package/templates/express-api/src/cars/routes.ts +58 -0
  22. package/templates/express-api/src/cars/store.ts +150 -0
  23. package/templates/express-api/src/cars/types.ts +8 -0
  24. package/templates/express-api/src/cars/validate.ts +16 -0
  25. package/templates/express-api/src/db.ts +13 -0
  26. package/templates/express-api/src/env.ts +23 -0
  27. package/templates/express-api/src/index.ts +37 -0
  28. package/templates/express-api/src/scripts/migrate.ts +13 -0
  29. package/templates/express-api/src/scripts/seed.ts +20 -0
  30. package/templates/express-api/test/validate.test.ts +25 -0
  31. package/templates/express-api/tsconfig.json +14 -0
  32. package/templates/hono-api/.env.example +13 -0
  33. package/templates/hono-api/README.md +77 -0
  34. package/templates/hono-api/docker-compose.yml +15 -0
  35. package/templates/hono-api/package.json +34 -0
  36. package/templates/hono-api/softeneers.template.json +17 -0
  37. package/templates/hono-api/src/auth/auth.ts +11 -0
  38. package/templates/hono-api/src/cars/demo.ts +10 -0
  39. package/templates/hono-api/src/cars/routes.ts +43 -0
  40. package/templates/hono-api/src/cars/store.ts +150 -0
  41. package/templates/hono-api/src/cars/types.ts +8 -0
  42. package/templates/hono-api/src/cars/validate.ts +16 -0
  43. package/templates/hono-api/src/db.ts +13 -0
  44. package/templates/hono-api/src/env.ts +23 -0
  45. package/templates/hono-api/src/index.ts +26 -0
  46. package/templates/hono-api/src/scripts/migrate.ts +13 -0
  47. package/templates/hono-api/src/scripts/seed.ts +20 -0
  48. package/templates/hono-api/test/validate.test.ts +25 -0
  49. package/templates/hono-api/tsconfig.json +14 -0
  50. package/templates/minimal/.env.example +2 -0
  51. package/templates/minimal/README.md +33 -0
  52. package/templates/minimal/package.json +22 -0
  53. package/templates/minimal/src/index.ts +20 -0
  54. package/templates/minimal/test/greet.test.ts +12 -0
  55. package/templates/minimal/tsconfig.json +15 -0
  56. package/templates/tanstack-start/.env.example +11 -0
  57. package/templates/tanstack-start/README.md +74 -0
  58. package/templates/tanstack-start/docker-compose.yml +15 -0
  59. package/templates/tanstack-start/package.json +56 -0
  60. package/templates/tanstack-start/public/favicon.ico +0 -0
  61. package/templates/tanstack-start/public/logo192.png +0 -0
  62. package/templates/tanstack-start/public/logo512.png +0 -0
  63. package/templates/tanstack-start/public/manifest.json +25 -0
  64. package/templates/tanstack-start/public/robots.txt +3 -0
  65. package/templates/tanstack-start/softeneers.template.json +17 -0
  66. package/templates/tanstack-start/src/cars/types.ts +8 -0
  67. package/templates/tanstack-start/src/cars/validate.ts +16 -0
  68. package/templates/tanstack-start/src/router.tsx +19 -0
  69. package/templates/tanstack-start/src/routes/__root.tsx +54 -0
  70. package/templates/tanstack-start/src/routes/api/auth/$.ts +14 -0
  71. package/templates/tanstack-start/src/routes/cars.tsx +87 -0
  72. package/templates/tanstack-start/src/routes/index.tsx +20 -0
  73. package/templates/tanstack-start/src/server/auth.ts +11 -0
  74. package/templates/tanstack-start/src/server/cars.ts +27 -0
  75. package/templates/tanstack-start/src/server/db.ts +12 -0
  76. package/templates/tanstack-start/src/server/demo.ts +9 -0
  77. package/templates/tanstack-start/src/server/env.ts +22 -0
  78. package/templates/tanstack-start/src/server/scripts/migrate.ts +13 -0
  79. package/templates/tanstack-start/src/server/scripts/seed.ts +20 -0
  80. package/templates/tanstack-start/src/server/store.ts +132 -0
  81. package/templates/tanstack-start/src/styles.css +17 -0
  82. package/templates/tanstack-start/tsconfig.json +28 -0
  83. package/templates/tanstack-start/tsr.config.json +3 -0
  84. package/templates/tanstack-start/vite.config.ts +14 -0
@@ -0,0 +1,43 @@
1
+ import { Hono } from "hono";
2
+
3
+ import { carStore } from "./store.js";
4
+ import { parseNewCar, ValidationError } from "./validate.js";
5
+
6
+ export const cars = new Hono();
7
+
8
+ cars.get("/", async (c) => {
9
+ return c.json(await carStore.list());
10
+ });
11
+
12
+ cars.get("/:id", async (c) => {
13
+ const car = await carStore.get(Number(c.req.param("id")));
14
+ if (!car) return c.json({ message: "Car not found." }, 404);
15
+ return c.json(car);
16
+ });
17
+
18
+ cars.post("/", async (c) => {
19
+ try {
20
+ const car = await carStore.create(parseNewCar(await c.req.json()));
21
+ return c.json(car, 201);
22
+ } catch (error) {
23
+ if (error instanceof ValidationError) return c.json({ message: error.message }, 400);
24
+ throw error;
25
+ }
26
+ });
27
+
28
+ cars.put("/:id", async (c) => {
29
+ try {
30
+ const car = await carStore.update(Number(c.req.param("id")), parseNewCar(await c.req.json()));
31
+ if (!car) return c.json({ message: "Car not found." }, 404);
32
+ return c.json(car);
33
+ } catch (error) {
34
+ if (error instanceof ValidationError) return c.json({ message: error.message }, 400);
35
+ throw error;
36
+ }
37
+ });
38
+
39
+ cars.delete("/:id", async (c) => {
40
+ const removed = await carStore.remove(Number(c.req.param("id")));
41
+ if (!removed) return c.json({ message: "Car not found." }, 404);
42
+ return c.body(null, 204);
43
+ });
@@ -0,0 +1,150 @@
1
+ import { DEMO_CARS } from "./demo.js";
2
+ import type { Car, NewCar } from "./types.js";
3
+
4
+ // The data layer behind the CRUD routes. With the `db` toggle on it persists to
5
+ // MySQL (Sequelize via @softeneers/db) and **falls back to an in-memory store if
6
+ // the database is unreachable**, so `npm run dev` always yields a working,
7
+ // pre-seeded demo. With `db` off it is always in-memory.
8
+ export interface CarStore {
9
+ list(): Promise<Car[]>;
10
+ get(id: number): Promise<Car | null>;
11
+ create(input: NewCar): Promise<Car>;
12
+ update(id: number, input: NewCar): Promise<Car | null>;
13
+ remove(id: number): Promise<boolean>;
14
+ }
15
+
16
+ // In-memory backend (the default, and the fallback when no database is reachable).
17
+ function createMemoryStore(seed = true): CarStore {
18
+ let nextId = 1;
19
+ const cars = new Map<number, Car>();
20
+ if (seed) {
21
+ for (const car of DEMO_CARS) {
22
+ const id = nextId++;
23
+ cars.set(id, { id, ...car });
24
+ }
25
+ }
26
+ return {
27
+ async list() {
28
+ return [...cars.values()].sort((a, b) => a.id - b.id);
29
+ },
30
+ async get(id) {
31
+ return cars.get(id) ?? null;
32
+ },
33
+ async create(input) {
34
+ const car: Car = { id: nextId++, ...input };
35
+ cars.set(car.id, car);
36
+ return car;
37
+ },
38
+ async update(id, input) {
39
+ const car = cars.get(id);
40
+ if (!car) return null;
41
+ const next: Car = { ...car, ...input };
42
+ cars.set(id, next);
43
+ return next;
44
+ },
45
+ async remove(id) {
46
+ return cars.delete(id);
47
+ },
48
+ };
49
+ }
50
+
51
+ // #if db
52
+ import { assertConnection, DataTypes, Model } from "@softeneers/db";
53
+
54
+ import { sequelize } from "../db.js";
55
+
56
+ class CarModel extends Model {
57
+ declare id: number;
58
+ declare brand: string;
59
+ declare model: string;
60
+ declare year: number;
61
+ }
62
+
63
+ CarModel.init(
64
+ {
65
+ id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true },
66
+ brand: { type: DataTypes.STRING, allowNull: false },
67
+ model: { type: DataTypes.STRING, allowNull: false },
68
+ year: { type: DataTypes.INTEGER, allowNull: false },
69
+ },
70
+ { sequelize, modelName: "Car", tableName: "cars" },
71
+ );
72
+
73
+ export { CarModel };
74
+
75
+ const toCar = (m: CarModel): Car => ({ id: m.id, brand: m.brand, model: m.model, year: m.year });
76
+
77
+ function createDbStore(): CarStore {
78
+ return {
79
+ async list() {
80
+ return (await CarModel.findAll({ order: [["id", "ASC"]] })).map(toCar);
81
+ },
82
+ async get(id) {
83
+ const m = await CarModel.findByPk(id);
84
+ return m ? toCar(m) : null;
85
+ },
86
+ async create(input) {
87
+ return toCar(await CarModel.create(input));
88
+ },
89
+ async update(id, input) {
90
+ const m = await CarModel.findByPk(id);
91
+ if (!m) return null;
92
+ await m.update(input);
93
+ return toCar(m);
94
+ },
95
+ async remove(id) {
96
+ const m = await CarModel.findByPk(id);
97
+ if (!m) return false;
98
+ await m.destroy();
99
+ return true;
100
+ },
101
+ };
102
+ }
103
+
104
+ // Resolve the backend once, lazily, on first use: try MySQL (create tables +
105
+ // seed if empty); on any connection error, fall back to the in-memory store.
106
+ let backend: Promise<CarStore> | null = null;
107
+ function resolveBackend(): Promise<CarStore> {
108
+ if (!backend) {
109
+ backend = (async () => {
110
+ try {
111
+ await assertConnection(sequelize);
112
+ await sequelize.sync();
113
+ const db = createDbStore();
114
+ if ((await db.list()).length === 0) {
115
+ for (const car of DEMO_CARS) await db.create(car);
116
+ }
117
+ console.log("Data store: MySQL");
118
+ return db;
119
+ } catch {
120
+ console.warn(
121
+ "Data store: in-memory — database unreachable. Run `docker compose up -d` (and `npm run db:migrate && npm run db:seed`) for MySQL.",
122
+ );
123
+ return createMemoryStore();
124
+ }
125
+ })();
126
+ }
127
+ return backend;
128
+ }
129
+
130
+ export const carStore: CarStore = {
131
+ async list() {
132
+ return (await resolveBackend()).list();
133
+ },
134
+ async get(id) {
135
+ return (await resolveBackend()).get(id);
136
+ },
137
+ async create(input) {
138
+ return (await resolveBackend()).create(input);
139
+ },
140
+ async update(id, input) {
141
+ return (await resolveBackend()).update(id, input);
142
+ },
143
+ async remove(id) {
144
+ return (await resolveBackend()).remove(id);
145
+ },
146
+ };
147
+ // #endif
148
+ // #if !db
149
+ export const carStore: CarStore = createMemoryStore();
150
+ // #endif
@@ -0,0 +1,8 @@
1
+ export interface Car {
2
+ id: number;
3
+ brand: string;
4
+ model: string;
5
+ year: number;
6
+ }
7
+
8
+ export type NewCar = Omit<Car, "id">;
@@ -0,0 +1,16 @@
1
+ import type { NewCar } from "./types.js";
2
+
3
+ /** Thrown when a request body is not a valid car. Mapped to HTTP 400. */
4
+ export class ValidationError extends Error {}
5
+
6
+ /** Validate and normalize an untrusted request body into a NewCar. */
7
+ export function parseNewCar(body: unknown): NewCar {
8
+ const b = (body ?? {}) as Record<string, unknown>;
9
+ const brand = typeof b.brand === "string" ? b.brand.trim() : "";
10
+ const model = typeof b.model === "string" ? b.model.trim() : "";
11
+ const year = Number(b.year);
12
+ if (!brand || !model || !Number.isInteger(year)) {
13
+ throw new ValidationError("brand (string), model (string) and year (integer) are required.");
14
+ }
15
+ return { brand, model, year };
16
+ }
@@ -0,0 +1,13 @@
1
+ import { createDb } from "@softeneers/db";
2
+
3
+ import { env } from "./env.js";
4
+
5
+ // A configured (but not-yet-connected) Sequelize instance. Importing a model
6
+ // registers it; `assertConnection`/`sync` open the connection on startup.
7
+ export const sequelize = createDb({
8
+ host: env.DB_HOST,
9
+ port: env.DB_PORT,
10
+ database: env.DB_NAME,
11
+ username: env.DB_USER,
12
+ password: env.DB_PASSWORD,
13
+ });
@@ -0,0 +1,23 @@
1
+ import "dotenv/config";
2
+
3
+ import { createEnv, z } from "@softeneers/env";
4
+
5
+ // Validated, typed environment. Boot fails fast with a readable message if the
6
+ // .env is wrong. Variables for a feature exist only when that toggle is on.
7
+ export const env = createEnv({
8
+ schema: {
9
+ PORT: z.coerce.number().default(4000),
10
+ CORS_ORIGIN: z.string().default("http://localhost:3000"),
11
+ // #if db
12
+ DB_HOST: z.string().default("127.0.0.1"),
13
+ DB_PORT: z.coerce.number().default(3306),
14
+ DB_NAME: z.string().default("app_dev"),
15
+ DB_USER: z.string().default("root"),
16
+ DB_PASSWORD: z.string().default(""),
17
+ // #endif
18
+ // #if auth
19
+ AUTH_SECRET: z.string().min(16).default("dev-secret-change-me-to-a-long-random-string"),
20
+ AUTH_BASE_URL: z.string().default("http://localhost:4000"),
21
+ // #endif
22
+ },
23
+ });
@@ -0,0 +1,26 @@
1
+ import { serve } from "@hono/node-server";
2
+ import { Hono } from "hono";
3
+ import { cors } from "hono/cors";
4
+
5
+ import { cars } from "./cars/routes.js";
6
+ import { env } from "./env.js";
7
+ // #if auth
8
+ import { auth } from "./auth/auth.js";
9
+ // #endif
10
+
11
+ const app = new Hono();
12
+
13
+ app.use("*", cors({ origin: env.CORS_ORIGIN }));
14
+
15
+ app.get("/health", (c) => c.json({ ok: true }));
16
+
17
+ app.route("/api/cars", cars);
18
+
19
+ // #if auth
20
+ // better-auth speaks the Fetch API, so hand it the raw Request and return its Response.
21
+ app.on(["GET", "POST"], "/api/auth/*", (c) => auth.handler(c.req.raw));
22
+ // #endif
23
+
24
+ serve({ fetch: app.fetch, port: env.PORT }, (info) => {
25
+ console.log(`API running on http://localhost:${info.port}`);
26
+ });
@@ -0,0 +1,13 @@
1
+ import "dotenv/config";
2
+
3
+ import { assertConnection } from "@softeneers/db";
4
+
5
+ import { sequelize } from "../db.js";
6
+ import "../cars/store.js"; // importing the store registers the Car model
7
+
8
+ const reset = process.argv.includes("--reset");
9
+
10
+ await assertConnection(sequelize);
11
+ await sequelize.sync({ force: reset });
12
+ console.log(reset ? "Database reset — tables recreated." : "Database synced — tables ensured.");
13
+ await sequelize.close();
@@ -0,0 +1,20 @@
1
+ import "dotenv/config";
2
+
3
+ import { assertConnection } from "@softeneers/db";
4
+
5
+ import { DEMO_CARS } from "../cars/demo.js";
6
+ import { CarModel } from "../cars/store.js";
7
+ import { sequelize } from "../db.js";
8
+
9
+ await assertConnection(sequelize);
10
+ await sequelize.sync();
11
+
12
+ const count = await CarModel.count();
13
+ if (count > 0) {
14
+ console.log(`Skipped seeding — ${count} cars already present.`);
15
+ } else {
16
+ await CarModel.bulkCreate([...DEMO_CARS]);
17
+ console.log(`Seeded ${DEMO_CARS.length} cars.`);
18
+ }
19
+
20
+ await sequelize.close();
@@ -0,0 +1,25 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+
4
+ import { parseNewCar, ValidationError } from "../src/cars/validate.ts";
5
+
6
+ test("parses a valid car body", () => {
7
+ assert.deepEqual(parseNewCar({ brand: "Tesla", model: "Model 3", year: 2023 }), {
8
+ brand: "Tesla",
9
+ model: "Model 3",
10
+ year: 2023,
11
+ });
12
+ });
13
+
14
+ test("trims strings and coerces a numeric year", () => {
15
+ assert.deepEqual(parseNewCar({ brand: " Ford ", model: " Mustang ", year: "1969" }), {
16
+ brand: "Ford",
17
+ model: "Mustang",
18
+ year: 1969,
19
+ });
20
+ });
21
+
22
+ test("rejects an incomplete body", () => {
23
+ assert.throws(() => parseNewCar({ brand: "Toyota" }), ValidationError);
24
+ assert.throws(() => parseNewCar({}), ValidationError);
25
+ });
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "rootDir": "src",
7
+ "outDir": "dist",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "skipLibCheck": true
12
+ },
13
+ "include": ["src"]
14
+ }
@@ -0,0 +1,2 @@
1
+ # Copied to .env by create-softeneers-app. Real .env files are never committed.
2
+ GREET_NAME=world
@@ -0,0 +1,33 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ A zero-framework **Node + TypeScript** starter generated by
4
+ [`create-softeneers-app`](https://www.npmjs.com/package/create-softeneers-app).
5
+ A clean blank slate: typed source in `src/`, a `node:test` suite, and a `tsc`
6
+ build — nothing else to unlearn.
7
+
8
+ ## Scripts
9
+
10
+ ```bash
11
+ npm run dev # run src/index.ts with watch (tsx)
12
+ npm run build # type-check + emit dist/ (tsc)
13
+ npm start # run the built dist/index.js
14
+ npm run typecheck # tsc --noEmit
15
+ npm test # node:test suite (via tsx)
16
+ ```
17
+
18
+ ## Layout
19
+
20
+ ```
21
+ src/index.ts your program (exports greet() as an example)
22
+ test/greet.test.ts the test suite
23
+ tsconfig.json strict TypeScript, NodeNext modules
24
+ .env.example copied to .env on generation (GREET_NAME)
25
+ ```
26
+
27
+ ## Try it
28
+
29
+ ```bash
30
+ npm install
31
+ npm test
32
+ npm run dev -- Ada # → "Hello, Ada!"
33
+ ```
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "minimal",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "Zero-framework Node + TypeScript starter generated by create-softeneers-app.",
7
+ "engines": {
8
+ "node": ">=18"
9
+ },
10
+ "scripts": {
11
+ "dev": "tsx watch src/index.ts",
12
+ "build": "tsc",
13
+ "start": "node dist/index.js",
14
+ "typecheck": "tsc --noEmit",
15
+ "test": "node --import tsx --test test/*.test.ts"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^20.0.0",
19
+ "tsx": "^4.19.0",
20
+ "typescript": "^5.6.0"
21
+ }
22
+ }
@@ -0,0 +1,20 @@
1
+ // Minimal Node + TypeScript starter. Replace this with your program — the
2
+ // structure (typed source in src/, a node:test suite, tsc build) is the point.
3
+
4
+ export interface GreetOptions {
5
+ /** Who to greet. Defaults to "world". */
6
+ name?: string;
7
+ /** Greeting word. Defaults to "Hello". */
8
+ greeting?: string;
9
+ }
10
+
11
+ export function greet({ name = "world", greeting = "Hello" }: GreetOptions = {}): string {
12
+ return `${greeting}, ${name}!`;
13
+ }
14
+
15
+ // Run directly (`npm run dev` / `npm start`) — but stay importable from tests.
16
+ const invokedDirectly = process.argv[1] && import.meta.url === `file://${process.argv[1]}`;
17
+ if (invokedDirectly) {
18
+ const name = process.argv[2] ?? process.env.GREET_NAME;
19
+ console.log(greet({ name }));
20
+ }
@@ -0,0 +1,12 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+
4
+ import { greet } from "../src/index.ts";
5
+
6
+ test("greets the world by default", () => {
7
+ assert.equal(greet(), "Hello, world!");
8
+ });
9
+
10
+ test("uses the provided name and greeting", () => {
11
+ assert.equal(greet({ name: "Ada", greeting: "Hi" }), "Hi, Ada!");
12
+ });
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "rootDir": "src",
7
+ "outDir": "dist",
8
+ "strict": true,
9
+ "declaration": true,
10
+ "esModuleInterop": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "skipLibCheck": true
13
+ },
14
+ "include": ["src"]
15
+ }
@@ -0,0 +1,11 @@
1
+ # #if db
2
+ DB_HOST=127.0.0.1
3
+ DB_PORT=3306
4
+ DB_NAME=app_dev
5
+ DB_USER=root
6
+ DB_PASSWORD=
7
+ # #endif
8
+ # #if auth
9
+ AUTH_SECRET=dev-secret-change-me-to-a-long-random-string
10
+ AUTH_BASE_URL=http://localhost:3000
11
+ # #endif
@@ -0,0 +1,74 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ A **TanStack Start** (fullstack React) app generated by
4
+ [`create-softeneers-app`](https://www.npmjs.com/package/create-softeneers-app),
5
+ with a full cars CRUD demo wired through type-safe **server functions**.
6
+
7
+ ## Scripts
8
+
9
+ ```bash
10
+ npm run dev # Vite dev server on http://localhost:3000
11
+ npm run build # production build (client + SSR)
12
+ npm start # preview the production build
13
+ npm run typecheck # generate routes + tsc --noEmit
14
+ npm test # vitest
15
+ # #if db
16
+ npm run db:migrate # create/sync tables
17
+ npm run db:seed # insert sample cars
18
+ npm run db:reset # drop, recreate, reseed
19
+ # #endif
20
+ ```
21
+
22
+ ## Structure
23
+
24
+ ```
25
+ src/routes/index.tsx home page
26
+ src/routes/cars.tsx cars CRUD UI (loader + server functions)
27
+ src/server/cars.ts createServerFn RPCs (list / create / remove)
28
+ src/server/store.ts data layer (MySQL or in-memory)
29
+ # #if auth
30
+ src/routes/api/auth/$.ts better-auth catch-all route (/api/auth/*)
31
+ src/server/auth.ts better-auth instance
32
+ # #endif
33
+ ```
34
+
35
+ The `/cars` page loads data with a route `loader` and mutates via server
36
+ functions — no hand-written API client, end-to-end type safety.
37
+ # #if db
38
+
39
+ ## Database
40
+
41
+ Persistence is MySQL via Sequelize ([`@softeneers/db`](https://www.npmjs.com/package/@softeneers/db)).
42
+ **`npm run dev` works immediately even without a database** — the server falls
43
+ back to a pre-seeded in-memory store when MySQL is unreachable. To switch on real
44
+ persistence, set `DB_*` in `.env` and start MySQL:
45
+
46
+ ```bash
47
+ # #if docker
48
+ docker compose up -d # start MySQL
49
+ # #endif
50
+ # The server auto-creates and seeds tables on first connect; these scripts give
51
+ # you explicit control:
52
+ npm run db:migrate && npm run db:seed
53
+ ```
54
+ # #endif
55
+ # #if auth
56
+
57
+ ## Authentication
58
+
59
+ Email + password auth via better-auth ([`@softeneers/auth`](https://www.npmjs.com/package/@softeneers/auth)),
60
+ served at `/api/auth/*`. Set a strong `AUTH_SECRET` in `.env`, and add the
61
+ `better-auth/react` client in your components to drive sign-in/up flows.
62
+ # #endif
63
+
64
+ ## Getting started
65
+
66
+ ```bash
67
+ npm install
68
+ npm run dev # a pre-seeded CRUD demo on http://localhost:3000 — no other setup
69
+ ```
70
+ # #if db
71
+
72
+ The app starts immediately on an in-memory demo and uses MySQL automatically once
73
+ it's reachable — see **Database** above to enable persistence.
74
+ # #endif
@@ -0,0 +1,15 @@
1
+ services:
2
+ db:
3
+ image: mysql:8
4
+ restart: unless-stopped
5
+ environment:
6
+ MYSQL_DATABASE: ${DB_NAME:-app_dev}
7
+ MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-}
8
+ MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
9
+ ports:
10
+ - "${DB_PORT:-3306}:3306"
11
+ volumes:
12
+ - db_data:/var/lib/mysql
13
+
14
+ volumes:
15
+ db_data:
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "tanstack-start",
3
+ "private": true,
4
+ "type": "module",
5
+ "imports": {
6
+ "#/*": "./src/*"
7
+ },
8
+ "scripts": {
9
+ "dev": "vite dev --port 3000",
10
+ "generate-routes": "tsr generate",
11
+ "build": "vite build",
12
+ "start": "vite preview --port 3000",
13
+ "typecheck": "tsr generate && tsc --noEmit",
14
+ "db:migrate": "tsx src/server/scripts/migrate.ts",
15
+ "db:seed": "tsx src/server/scripts/seed.ts",
16
+ "db:reset": "tsx src/server/scripts/migrate.ts --reset && tsx src/server/scripts/seed.ts",
17
+ "test": "vitest run --passWithNoTests"
18
+ },
19
+ "dependencies": {
20
+ "@softeneers/auth": "^0.1.0",
21
+ "@softeneers/db": "^0.1.0",
22
+ "@softeneers/env": "^0.1.0",
23
+ "@tailwindcss/vite": "^4.1.18",
24
+ "@tanstack/react-devtools": "latest",
25
+ "@tanstack/react-router": "latest",
26
+ "@tanstack/react-router-devtools": "latest",
27
+ "@tanstack/react-router-ssr-query": "latest",
28
+ "@tanstack/react-start": "latest",
29
+ "@tanstack/router-plugin": "^1.132.0",
30
+ "dotenv": "^16.4.5",
31
+ "lucide-react": "^0.545.0",
32
+ "mysql2": "^3.11.0",
33
+ "react": "^19.2.0",
34
+ "react-dom": "^19.2.0",
35
+ "tailwindcss": "^4.1.18"
36
+ },
37
+ "devDependencies": {
38
+ "@tailwindcss/typography": "^0.5.16",
39
+ "@tanstack/devtools-vite": "latest",
40
+ "@tanstack/router-cli": "^1.132.0",
41
+ "@testing-library/dom": "^10.4.1",
42
+ "@testing-library/react": "^16.3.0",
43
+ "@types/node": "^22.10.2",
44
+ "@types/react": "^19.2.0",
45
+ "@types/react-dom": "^19.2.0",
46
+ "@vitejs/plugin-react": "^6.0.1",
47
+ "jsdom": "^28.1.0",
48
+ "tsx": "^4.19.0",
49
+ "typescript": "^6.0.2",
50
+ "vite": "^8.0.0",
51
+ "vitest": "^4.1.5"
52
+ },
53
+ "pnpm": {
54
+ "onlyBuiltDependencies": ["esbuild", "lightningcss"]
55
+ }
56
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "short_name": "TanStack App",
3
+ "name": "Create TanStack App Sample",
4
+ "icons": [
5
+ {
6
+ "src": "favicon.ico",
7
+ "sizes": "64x64 32x32 24x24 16x16",
8
+ "type": "image/x-icon"
9
+ },
10
+ {
11
+ "src": "logo192.png",
12
+ "type": "image/png",
13
+ "sizes": "192x192"
14
+ },
15
+ {
16
+ "src": "logo512.png",
17
+ "type": "image/png",
18
+ "sizes": "512x512"
19
+ }
20
+ ],
21
+ "start_url": ".",
22
+ "display": "standalone",
23
+ "theme_color": "#000000",
24
+ "background_color": "#ffffff"
25
+ }
@@ -0,0 +1,3 @@
1
+ # https://www.robotstxt.org/robotstxt.html
2
+ User-agent: *
3
+ Disallow: