@deepraxis/app-template 0.0.0-stage → 0.1.0-dev.2990b29

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 (54) hide show
  1. package/LICENSE +67 -0
  2. package/README.md +6 -2
  3. package/SKILL.md +145 -0
  4. package/package.json +13 -4
  5. package/references/design.md +113 -0
  6. package/references/research.md +56 -0
  7. package/template/README.md +57 -0
  8. package/template/_gitignore +7 -0
  9. package/template/app/account/page.tsx +47 -0
  10. package/template/app/admin/page.tsx +53 -0
  11. package/template/app/api/auth/login/route.ts +30 -0
  12. package/template/app/api/auth/register/route.ts +24 -0
  13. package/template/app/api/health/route.ts +10 -0
  14. package/template/app/api/images/route.ts +15 -0
  15. package/template/app/globals.css +45 -0
  16. package/template/app/images/[...key]/route.ts +20 -0
  17. package/template/app/layout.tsx +35 -0
  18. package/template/app/login/page.tsx +26 -0
  19. package/template/app/logout/route.ts +9 -0
  20. package/template/app/page.tsx +21 -0
  21. package/template/app/register/page.tsx +25 -0
  22. package/template/components/ui/badge.tsx +26 -0
  23. package/template/components/ui/button.tsx +24 -0
  24. package/template/components/ui/card.tsx +17 -0
  25. package/template/components/ui/input.tsx +14 -0
  26. package/template/components/ui/label.tsx +6 -0
  27. package/template/env.d.ts +11 -0
  28. package/template/lib/auth.test.ts +61 -0
  29. package/template/lib/auth.ts +107 -0
  30. package/template/lib/db.ts +19 -0
  31. package/template/lib/design-tokens.test.ts +90 -0
  32. package/template/lib/format.test.ts +12 -0
  33. package/template/lib/format.ts +18 -0
  34. package/template/lib/guard.ts +37 -0
  35. package/template/lib/migrate.test.ts +50 -0
  36. package/template/lib/migrate.ts +54 -0
  37. package/template/lib/migrations.ts +5 -0
  38. package/template/lib/schema.ts +30 -0
  39. package/template/lib/session.ts +48 -0
  40. package/template/lib/storage.ts +32 -0
  41. package/template/lib/users.test.ts +51 -0
  42. package/template/lib/users.ts +46 -0
  43. package/template/lib/utils.ts +6 -0
  44. package/template/migrations/0001_init.sql +27 -0
  45. package/template/next.config.ts +4 -0
  46. package/template/package.json +39 -0
  47. package/template/postcss.config.cjs +1 -0
  48. package/template/praxis.jsonc +10 -0
  49. package/template/tailwind.config.ts +35 -0
  50. package/template/test/fake-d1.ts +38 -0
  51. package/template/tsconfig.json +17 -0
  52. package/template/vinext-plugins.ts +77 -0
  53. package/template/vite.config.ts +8 -0
  54. package/template/wrangler.jsonc +11 -0
package/LICENSE ADDED
@@ -0,0 +1,67 @@
1
+ Deepractice Commercial Proprietary License
2
+
3
+ Copyright (c) 2026 南京深度实践人工智能科技有限公司.
4
+ Unified Social Credit Code: 91430104MAEDATJ09G.
5
+ All rights reserved.
6
+
7
+ IMPORTANT: THIS IS NOT AN OPEN SOURCE LICENSE.
8
+
9
+ This repository and its source code, documentation, architecture, tests,
10
+ scripts, configuration, prompts, agent designs, protocols, workflows, and other
11
+ accompanying materials are proprietary commercial assets owned by 南京深度实践
12
+ 人工智能科技有限公司 (the "Copyright Holder").
13
+ The Copyright Holder's Unified Social Credit Code is 91430104MAEDATJ09G.
14
+
15
+ The purpose of this license is to bind this repository and the project code to
16
+ the Copyright Holder as closed-source commercial intellectual property.
17
+
18
+ No rights are granted by default. Access to this repository, possession of a
19
+ copy of the source code, visibility into commit history, participation in
20
+ business discussions, employment, consulting, shareholder status, investment,
21
+ or cooperation with any related entity does not grant any ownership interest,
22
+ copyright license, patent license, trademark license, trade secret license, or
23
+ other intellectual property right.
24
+
25
+ Without a separate written commercial agreement signed by the Copyright Holder,
26
+ no person or entity may, directly or indirectly:
27
+
28
+ 1. use, run, deploy, host, or provide the software as a service;
29
+ 2. copy, modify, merge, adapt, translate, or create derivative works;
30
+ 3. publish, distribute, sublicense, lease, sell, transfer, or disclose the
31
+ software or any substantial part of it;
32
+ 4. reverse engineer, decompile, disassemble, or otherwise attempt to derive
33
+ source, structure, architecture, protocols, prompts, trade secrets, or
34
+ implementation details;
35
+ 5. use the software, code, documentation, architecture, protocols, prompts, or
36
+ repository history to train, fine-tune, evaluate, or improve any AI model,
37
+ coding agent, software-generation system, or competing product;
38
+ 6. remove, obscure, or alter copyright, license, ownership, confidentiality, or
39
+ attribution notices.
40
+
41
+ All commercial use, internal business use, customer delivery, private
42
+ deployment, SaaS operation, resale, redistribution, integration into another
43
+ product, or access by third parties requires prior written authorization from
44
+ the Copyright Holder.
45
+
46
+ Contributions are accepted only on the basis that the contributor has the right
47
+ to contribute them and assigns, or agrees to assign, all applicable rights in
48
+ those contributions to the Copyright Holder unless a separate written agreement
49
+ signed by the Copyright Holder states otherwise.
50
+
51
+ This license applies to this repository as a whole. No part of this repository
52
+ may be treated as public domain, community property, jointly owned project
53
+ property, or an implied asset of any company, shareholder, collaborator,
54
+ customer, investor, or business partner other than the Copyright Holder without
55
+ a separate written agreement signed by the Copyright Holder.
56
+
57
+ The Chinese legal name "南京深度实践人工智能科技有限公司" is the authoritative
58
+ identity of the Copyright Holder. Any English rendering of that name is for
59
+ identification convenience only and does not create a separate legal entity or
60
+ alter ownership.
61
+
62
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
63
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
64
+ FOR A PARTICULAR PURPOSE, TITLE, AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
65
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING
66
+ FROM OR IN CONNECTION WITH THE SOFTWARE OR THE USE OF OTHER DEALINGS IN THE
67
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,7 @@
1
- # Temporary Holding Version
1
+ # @deepraxis/app-template
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The standard Praxis application template: a vinext (Next.js App Router on Workers) app with
4
+ end-user sign-up and sign-in, D1 via drizzle, R2 image upload and an admin area.
5
+ `praxis app create` copies `template/` into a new project and replaces the `<app-name>` placeholder
6
+ in `praxis.jsonc`; an AI agent or a developer then adapts it. See `template/README.md` for the stack.
7
+ It has no runtime code and no private dependency; `spec/` holds the develop proof and is not published.
package/SKILL.md ADDED
@@ -0,0 +1,145 @@
1
+ ---
2
+ name: build-application
3
+ description: "Required before building, changing, deploying or operating a web application on Praxis: a website, booking or ordering site, store, member area, internal tool, or any app with pages, sign-in, a database or uploads. Uses the standard Praxis stack (Next.js App Router on Workers via vinext, D1, R2) with `praxis app create` and `praxis app deploy`. Never deploy with wrangler and never pick another framework."
4
+ ---
5
+
6
+ # Build an application on Praxis
7
+
8
+ Use this skill when the user wants a working web application: something with pages people open in a browser, and usually sign-in, stored data or uploads. The result is a live URL on Praxis that the user can open and share.
9
+
10
+ The stack and the architecture are fixed. Do not compare frameworks, do not ask the user to choose one, and do not deploy any other way. Every app starts from the standard template and is shipped with `praxis app deploy`.
11
+
12
+ ## The standard stack
13
+
14
+ | Part | What it is | Where it lives in the project |
15
+ |---|---|---|
16
+ | Pages and API | Next.js App Router (v15), run on Cloudflare Workers by vinext | `app/` |
17
+ | Business rules | Plain TypeScript, server-only | `lib/` |
18
+ | Database | D1 (SQLite) with drizzle-orm; SQL migrations applied automatically on the first request | `migrations/`, `lib/migrations.ts`, `lib/schema.ts` |
19
+ | Files | R2 bucket `IMAGES` | `lib/storage.ts` |
20
+ | End-user accounts | Username and password; signed HttpOnly session cookie; the first account to register becomes `admin` | `lib/auth.ts`, `lib/session.ts`, `lib/users.ts`, `lib/guard.ts` |
21
+ | UI | Tailwind CSS with Button, Input, Label and Card in `components/ui/` | `components/ui/` |
22
+ | Validation | zod | in each route handler |
23
+ | Project file | `praxis.jsonc`: name, secrets, health check | project root |
24
+
25
+ The template already contains sign-up, sign-in, sign-out, an account page with image upload, an admin page and `/api/health`. Build the user's app by changing and extending it, not by starting over.
26
+
27
+ ## Where to run commands
28
+
29
+ - **You are a Praxis platform agent.** Run `praxis` in your machine's terminal (the Bash tool). The CLI is preinstalled and already signed in through the `PRAXIS_API_TOKEN` environment variable, so never run `praxis login`. For questions about deployed apps (versions, deployments, events, secrets) you can also call the SDK from codemode: `praxis.application.workers.list()`, `praxis.application.workers.deployments.list({ path: { worker_id } })` and so on.
30
+ - **You run elsewhere (Claude Code, Cursor, CI).** Install the CLI with `npm install -g @deepraxis/cli`. Sign in with `praxis login`, or set `PRAXIS_API_KEY`. Use the CLI for everything. Its environment follows the install: `@deepraxis/cli` talks to production, `@deepraxis/cli@dev` to develop; `PRAXIS_ENV=develop|production` overrides it. Use develop only when the user asks for it.
31
+
32
+ ## Steps
33
+
34
+ ### 1. Decide who you are working with, then gather requirements to match
35
+ Read the request once and pick one of three paths. When in doubt, take **Quick build** and say so in one sentence ("I'll build it with sensible defaults; tell me anytime if you want something specific").
36
+
37
+ | Path | Signals in the request | How much to ask before building |
38
+ |---|---|---|
39
+ | **Quick build** | Describes a business outcome only ("make me a booking site for my pet shop"); no technical terms | Ask at most two questions, and only those whose answer changes the data model (for example "can one customer book several pets in one visit?"). Ask them in one go with the `ask_user_question` tool, offering 2–4 concrete options each. Choose every other detail yourself (names, prices, time slots, colors) and list those defaults in the final report so the user can correct them. |
40
+ | **Build with a developer** | Mentions a stack, tables or fields, an API, a repository or existing code, or asks to see a plan or design first | Before writing code, send a short plan and wait for the user to confirm or edit it. The plan covers: pages; tables with their columns; roles and who may do what; bindings and secrets; anything that departs from the template; and what you will not build. After confirmation, build exactly that, and check back before changing the plan. |
41
+ | **Change an existing app** | There is already a `praxis.jsonc` in the working directory, or the user names an app they have | Skip `praxis app create`. Read the project's `README.md` and `praxis.jsonc`, make the change, then continue at step 4 below. |
42
+
43
+ Whatever the path, write down for yourself:
44
+ - the pages;
45
+ - the data, which becomes tables and columns;
46
+ - who may do what, as roles (`user` and `admin` already exist);
47
+ - what is uploaded.
48
+
49
+ A developer who wants a different framework or brings an existing project: say plainly what the platform runs (Workers with D1, KV, R2, vars, secrets and static files; see Limits). Also say that `praxis app deploy` today ships projects with the template's build layout: a vinext build that writes `dist/server/wrangler.json`, `dist/server/*.js` and `dist/client`, plus a `praxis.jsonc`. Offer to move their pages and logic onto the template. Do not claim another layout will deploy.
50
+
51
+ ### 2. Create the project
52
+ ```bash
53
+ praxis app create <name>
54
+ cd <name>
55
+ ```
56
+ `<name>` becomes part of the public URL: 3–40 lowercase letters, digits or single hyphens. `www`, `api`, `admin`, `app`, `docs`, `console` and `status` are reserved. The command copies the template and installs the dependencies.
57
+
58
+ Read `README.md` in the new project once. It has the folder map and the recipes below in more detail.
59
+
60
+ ### 3. Develop
61
+ Before you write pages, read the [design guide](references/design.md): it says how to choose the look, which layout fits the kind of app, and how it must work on phones and in Chinese. Before you build anything beyond plain pages, forms and tables (a calendar, charts, maps, rich text, export, payments …), follow [find before you build](references/research.md): look at how others do it, pick an existing library or shadcn/ui component, and prove it with a short spike.
62
+
63
+ - **Tables**: add `migrations/0002_<what>.sql` using `CREATE TABLE IF NOT EXISTS …`. Import it with `?raw` and append it to the list in `lib/migrations.ts`, then describe the table in `lib/schema.ts`. Migrations run once, in order, on the first request after a deploy. **Never edit a migration that has already been deployed**; add a new one instead.
64
+ - **Rules** (prices, availability, who owns what) go in `lib/`. Pages and route handlers only call them.
65
+ - **Pages**: `app/<path>/page.tsx`. Protect them with `await requireUser("/<path>")` or `await requireAdmin("/<path>")` from `lib/guard.ts`.
66
+ - **Forms**: post to a route handler in `app/api/…/route.ts`. Validate with zod and answer with a 303 redirect (`redirectResponse` in `lib/guard.ts`), the way the sign-in form does.
67
+ - **Uploads**: follow `app/api/images/route.ts` and `lib/storage.ts`. That code checks type and size, stores the file under the owner and records it in D1. For an upload that belongs to a record (a pet photo on a booking, a cover on a book), read the file from the same form, call `checkImage` and `saveImage`, and store the returned `key` in a column of your row. Images are served by `/images/<key>` to their owner and admins only. If something must be public (a gallery, product photos), add a separate route that serves only keys you have marked public in your table, and leave `/images` private.
68
+ - **New bindings**: declare them in `wrangler.jsonc` (`d1_databases`, `kv_namespaces`, `r2_buckets`, `vars`), add the type to `env.d.ts`, and read them with `import { env } from "cloudflare:workers"`.
69
+ - **Secrets**: add them to `praxis.jsonc` `secrets`. `"generate"` creates a random value on the first deploy and keeps it on every later deploy. Never write a secret value into a file.
70
+ - **UI**: use the components in `components/ui/` and the semantic Tailwind classes (`bg-primary`, `text-muted-foreground` …). Change the look through the tokens in `app/globals.css`, never with raw colors in pages. Change the site name, home page, navigation and colors freely; keep the sign-in, sign-up, account and admin pages working.
71
+
72
+ ### 4. Check before deploying
73
+ ```bash
74
+ bun run typecheck && bun test && bun run build
75
+ ```
76
+ `bun test` runs every test in the project. Fix every error before you deploy. `bun run dev` serves the app locally if you want to look at it first.
77
+
78
+ ### 5. Deploy
79
+ ```bash
80
+ praxis app deploy
81
+ ```
82
+ It builds, uploads only the changed files, creates a version, deploys it, keeps or creates the secrets and calls the `healthcheck` path. The build log comes first; the result comes last and contains the URL. With `--json` the last line is a JSON object with `url`, `worker_id`, `version_id` and `secrets_created`. A deploy takes about one to two minutes.
83
+
84
+ ### 6. Verify it like a user
85
+ - Open the URL. If the `control-browser` skill is available, use it, and walk through the main flow: register, sign in, do the main task, and check the admin page. Take screenshots at 390 px and 1280 px wide and fix what looks off before you report (design guide, section 8).
86
+ - Without a browser, check over HTTP. Forms post `application/x-www-form-urlencoded` (uploads: `multipart/form-data`) and answer `303` with a `Location`; protected pages answer a redirect to `/login` when signed out.
87
+ ```bash
88
+ U=<url>; J=$(mktemp)
89
+ curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' -c $J -d 'username=admin&password=<password>' $U/api/auth/register
90
+ curl -s -b $J $U/admin | grep -c '<something only admins see>'
91
+ curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' $U/admin # signed out: redirect to /login
92
+ ```
93
+ Match on plain words, not on numbers next to markup: React may split "Books (2)" into several text nodes in the HTML.
94
+ - **The first account registered on a fresh app becomes the admin.** Register that account yourself with the username the user asked for (or `admin`) and a strong random password, then hand both to the user. Any test accounts you create after that are ordinary users; mention them so the user can ignore them.
95
+
96
+ ### 7. Report to the user
97
+ Give:
98
+ - the URL;
99
+ - the admin username and password;
100
+ - what the app does, in a few lines;
101
+ - how to ask for changes.
102
+
103
+ Keep technical words out of the report (no "worker", "D1" or "migration").
104
+
105
+ ## Changing the app later
106
+ Edit the code, then run step 4 and step 5 again. The data in D1 and R2 stays across deploys.
107
+
108
+ Rollback switches the code, not the data: rows written by the newer version stay in D1 and migrations are not undone. So write migrations that the previous version still works with (add tables and columns; do not rename or drop them in the same release).
109
+
110
+ If a deploy breaks something, go back to the previous version at once and then fix forward:
111
+ ```bash
112
+ praxis app rollback # the version before the current one
113
+ praxis app rollback --to <version_id>
114
+ ```
115
+
116
+ ## Operating the app
117
+ | Question | CLI | Codemode |
118
+ |---|---|---|
119
+ | Which versions exist? | `praxis api application worker version list --worker <worker_id>` | `praxis.application.workers.versions.list({ path: { worker_id } })` |
120
+ | What was deployed, and when? | `praxis api application worker deployment list --worker <worker_id>` | `praxis.application.workers.deployments.list({ path: { worker_id } })` |
121
+ | What happened (deploys, failures)? | `praxis api application worker event list --worker <worker_id>` | `praxis.application.workers.events.list({ path: { worker_id } })` |
122
+ | Which secrets are set? | `praxis api application worker secret list --worker <worker_id>` | `praxis.application.workers.secrets.list({ path: { worker_id } })` |
123
+ | Find the worker id | `praxis api application worker list` | `praxis.application.workers.list()` |
124
+ | Delete the app | `praxis api application worker delete <worker_id> --yes` | `praxis.application.workers.delete({ path: { worker_id } })` |
125
+
126
+ Deleting takes the URL offline at once. The app, its database and its files are kept for 7 days and can be brought back with `praxis api application worker restore <worker_id>`; after that they are purged. A deleted app still appears in `worker list` with `praxis.status` `deleted` until it is purged. Only delete when the user asks for it.
127
+
128
+ Runtime logs are not available yet. To find out why a page fails, reproduce the failure, read the error page or the API response, and add clear error messages in the route handler.
129
+
130
+ ## Limits
131
+ - **Not supported**: service bindings, queues, Durable Objects, Workers AI, Analytics Engine. A deploy that declares one fails with `UNSUPPORTED` and names it. Use D1, KV, R2, vars, secrets and static files instead.
132
+ - **Rollout**: there is no gradual rollout. A deploy replaces the live version.
133
+ - **Files**: each file can be at most 25 MiB. The template limits image uploads to 5 MB.
134
+ - **Template parts to leave alone**: `vinext-plugins.ts` and `vite.config.ts`. They make the build deployable; removing them breaks deploys with "No such module".
135
+
136
+ ## When something fails
137
+ | Symptom | Cause | Fix |
138
+ |---|---|---|
139
+ | `praxis: command not found` | The CLI is not installed | `npm install -g @deepraxis/cli` |
140
+ | `NOT_LOGGED_IN`, or 401 on a platform agent's machine | The machine credentials are missing | Run the command again. If it persists, tell the user the machine has no platform credentials. Do not run `praxis login` there. |
141
+ | `UNSUPPORTED … services` (or queues and so on) | A binding the platform does not offer | Remove it from `wrangler.jsonc` and use D1, KV or R2 |
142
+ | Health check failed after the deploy | The app throws on `/api/health`, usually a broken migration | Open the URL to read the error, fix the migration with a NEW migration file, deploy again; `praxis app rollback` meanwhile |
143
+ | Sign-in answers 500 "SESSION_SECRET is not configured" | The secret was removed from `praxis.jsonc` | Put `"SESSION_SECRET": "generate"` back and deploy |
144
+ | A build error about modules or `import.meta.url` | `vinext-plugins.ts` was changed or removed | Restore it from the template |
145
+ | Deploy fails with `Completion token has already been consumed` | A rare platform retry issue | Run `praxis app deploy` again |
package/package.json CHANGED
@@ -1,6 +1,15 @@
1
1
  {
2
2
  "name": "@deepraxis/app-template",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0-dev.2990b29",
4
+ "description": "The standard Praxis application template: vinext + D1 + R2 + end-user auth + admin area.",
5
+ "license": "Apache-2.0",
6
+ "files": [
7
+ "template",
8
+ "README.md",
9
+ "SKILL.md",
10
+ "references"
11
+ ],
12
+ "publishConfig": {
13
+ "access": "public"
14
+ }
15
+ }
@@ -0,0 +1,113 @@
1
+ # Designing the app's interface
2
+
3
+ Read this before you build or restyle pages. The people who use these apps are the merchant's own customers and staff, not developers. Most customers arrive on a phone, often inside WeChat's in-app browser. Staff use the admin area on a laptop. The goal is an interface that looks like it was made for this business, works with one thumb, and never makes a customer wonder what to do next.
4
+
5
+ ## 1. Decide the look before writing markup
6
+
7
+ Write a five-line design brief for yourself, grounded in the business, not in "modern and clean":
8
+ - **Who and where**: for example "pet owners booking on their phone between errands" or "front-desk staff checking today's list on a laptop".
9
+ - **Feel**: two or three adjectives taken from the business itself. A pet groomer is warm and playful, a law office calm and formal, a bakery homemade and appetizing.
10
+ - **Palette**: 4–6 colors with names and hex values. One brand color for the primary action and the current selection, one neutral family for text and surfaces, and the status colors (success, warning, danger). Pick the brand color from the business (its sign, its product, its logo), not purple by default.
11
+ - **Type**: see section 4. Mostly size, weight and spacing; the font family is fixed.
12
+ - **One memorable thing**: choose the single element that carries the personality, such as a big photo on the home page, an illustrated service card, or a bold headline. Keep everything else quiet.
13
+
14
+ Put the palette into the design tokens in `app/globals.css` and `tailwind.config.ts` (section 2). Never scatter hex values through pages.
15
+
16
+ ### Looks that read as "generated"
17
+ Avoid these unless the user asks for them:
18
+ - a purple-to-blue gradient hero;
19
+ - every section chopped into identical rounded cards with the same grey shadow;
20
+ - a small all-caps label above every heading;
21
+ - one word in a headline highlighted in another color;
22
+ - emoji used as icons;
23
+ - fade-and-slide-up animation on every section;
24
+ - "→" appended to every button.
25
+
26
+ ## 2. Tokens: change the look in one place
27
+
28
+ The template's colors and radius live as CSS variables in `app/globals.css` (`--background`, `--foreground`, `--muted`, `--muted-foreground`, `--border`, `--primary`, `--primary-foreground`, `--accent`, `--destructive`, `--success`, `--warning`, `--radius`). `tailwind.config.ts` maps them to classes such as `bg-primary`, `text-muted-foreground` and `border-border`.
29
+
30
+ - Pages and components use only these semantic classes, never raw palette classes such as `bg-slate-900` or `text-pink-500`.
31
+ - To rebrand, edit the variables. A site name, a brand color and a radius change should touch only `globals.css`, `tailwind.config.ts` and `app/layout.tsx`.
32
+ - Every background color has a matching foreground for text on it, with at least 4.5:1 contrast. Check the brand color against white text before you use it on buttons.
33
+
34
+ ## 3. Layouts by kind of app
35
+
36
+ Pick the pattern that matches the request, then adapt it.
37
+
38
+ **Booking and services** (groomer, salon, clinic, class, repair):
39
+ - Home: what the business offers, a photo, and one primary button, "Book now".
40
+ - Service list: cards with name, price, duration and a one-line description.
41
+ - Booking flow:
42
+ 1. Choose a service.
43
+ 2. Pick a day from a horizontal strip of the next days, with the weekday shown.
44
+ 3. Pick a time from a grid of tappable time chips. Show full slots as disabled; do not hide them.
45
+ 4. Add details.
46
+ 5. Confirm.
47
+ Never use a plain `<select>` for dates or times.
48
+ - Confirmation page: a summary of what, when, where and the price, plus what happens next.
49
+ - "My bookings": upcoming first, each with a status badge and the actions that are allowed.
50
+
51
+ **Store and catalog** (shop, menu, product list):
52
+ - A grid of product cards, two columns on phones, with photo, name and price.
53
+ - A detail page with a large photo and one primary action.
54
+ - The cart or order summary is reachable from every page.
55
+
56
+ **Membership and content** (club, community, course):
57
+ - A feed or list of items with clear ownership ("added by …").
58
+ - A profile or account page with the member's own items.
59
+ - Creating content is one obvious button.
60
+
61
+ **Admin area** (every app has one):
62
+ - Laptop first. A left sidebar or top tabs for sections.
63
+ - Up top, the few numbers that matter today (today's bookings, pending, new members), each linking to the filtered list.
64
+ - Tables with filters by status, a search box, status badges, and row actions on the right.
65
+ - Put the next action first: "3 bookings waiting for confirmation" with a button, not just a chart.
66
+ - Destructive actions (cancel, delete) ask for confirmation.
67
+
68
+ **Lead or registration form** (event sign-up, enquiry):
69
+ - One column with only the fields that are needed.
70
+ - A clear thank-you page that says what happens next.
71
+
72
+ ## 4. Phones and Chinese text
73
+
74
+ - **Mobile first.** Design at 390 px wide first, then widen. Use one column on phones. The primary action on long forms and booking steps sits in a bar fixed to the bottom of the screen.
75
+ - **Tap targets** are at least 44×44 px, with at least 8 px between them. Nothing may depend on hover.
76
+ - **Inputs** use the right type (`tel`, `email`, `number`, `date`) so phones show the right keyboard. Labels sit above fields, never only as a placeholder.
77
+ - **WeChat in-app browser**: avoid fixed headers taller than 56 px, do not rely on new tabs or pop-ups, and keep pages fast because the network may be slow. Use no web fonts for Chinese (section below) and compress uploaded images.
78
+ - **Chinese type**:
79
+ - Use the system font stack only: `-apple-system, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", sans-serif`. Chinese web fonts weigh several megabytes, so do not load them.
80
+ - Body text is 16 px on phones with line height 1.7.
81
+ - Headings are bold, not huge (20–28 px on phones).
82
+ - Do not use letter-spacing, italics or all-caps on Chinese text.
83
+ - Use Chinese punctuation in Chinese sentences.
84
+ - **Language**: write the interface in the user's language. For a Chinese request, every label, button, message and date is Chinese, and `app/layout.tsx` sets `<html lang="zh-CN">`. Show dates as `10月28日 周三`, times as `14:00`, money as `¥228`. Format dates, times and sizes through `lib/format.ts` (set its `LOCALE` and `TIME_ZONE` for the audience), never by printing raw ISO strings or byte counts.
85
+
86
+ ## 5. Every screen has four states
87
+
88
+ - **Empty**: say why it is empty and offer the next action ("还没有预约 · 去预约第一次洗护"). Never show an empty table with headers only.
89
+ - **Loading**: keep the layout steady; use skeleton blocks for lists.
90
+ - **Error**: say what went wrong and how to fix it, next to the thing that failed. Keep what the user typed: when a form fails validation, re-render the form with the values and the field errors, instead of redirecting to an empty form.
91
+ - **Success**: confirm with the same verb as the button. "确认预约" leads to "预约已确认", followed by where to go next.
92
+
93
+ Disable a submit button while it is submitting, so a double tap cannot create two bookings.
94
+
95
+ ## 6. Words on the screen
96
+
97
+ - Name things the way customers say them ("预约", "我的宠物"), not the way the code does ("appointment record", "entity").
98
+ - Buttons say exactly what happens: "确认预约", not "提交".
99
+ - Never show ids, raw status codes, English enum values or stack traces to customers. Map statuses to words and colors: 待确认 (warning), 已确认 (primary), 已完成 (success), 已取消 (muted).
100
+ - No filler paragraphs that explain the interface.
101
+
102
+ ## 7. Quality floor
103
+
104
+ - Contrast is at least 4.5:1 for text.
105
+ - Focus is visible for keyboard users.
106
+ - Every image has an `alt`.
107
+ - Buttons are `<button>` and links are `<a>`.
108
+ - Forms work without JavaScript, which the template's plain form posts already do.
109
+ - Pages use no horizontal scroll at 360 px.
110
+
111
+ ## 8. Look at it before you report
112
+
113
+ If the `control-browser` skill is available, take screenshots of the home page, the main flow and the admin area at 390 px and at 1280 px wide, and fix what looks off: overflowing text, cramped buttons, misaligned columns, an empty state that shows nothing. Remove one decoration before you finish. Without a browser, at least fetch every page over HTTP and check that it renders the real content and the Chinese text you expect.
@@ -0,0 +1,56 @@
1
+ # Find before you build: research and spike
2
+
3
+ Before you build anything beyond plain pages, forms and tables, find out how others solve it, pick an existing library or component, and prove it works here with a small spike. Typical cases:
4
+ - a date or time picker, calendar view or scheduling grid;
5
+ - charts;
6
+ - maps or address input;
7
+ - rich text or Markdown editing;
8
+ - drag and drop;
9
+ - image cropping;
10
+ - QR codes;
11
+ - CSV or Excel export;
12
+ - PDF output;
13
+ - search;
14
+ - payments;
15
+ - notifications.
16
+
17
+ Hand-writing these costs hours and usually ends up worse than an existing library.
18
+
19
+ ## 1. Look for the pattern (5 minutes)
20
+
21
+ - **How do well-known products present it?** With the `control-browser` skill, open one or two products that do the same job and take screenshots. A booking flow might come from a salon or clinic booking site; an order list from a delivery or shop admin. Note what to copy: step order, which fields, how errors and empty states look. Copy the structure, never the brand.
22
+ - **Is it already in the template?** Check `components/ui/` and `lib/` first, and reuse what is there.
23
+
24
+ ## 2. Pick a candidate (5 minutes)
25
+
26
+ Prefer, in this order:
27
+ 1. **shadcn/ui components and blocks** (ui.shadcn.com). Copy the component source into `components/ui/`. They match the template's Tailwind and token setup. Most rely on Radix UI primitives (`@radix-ui/react-*`); install the ones a component imports.
28
+ 2. **A focused, popular npm package** for the job, for example `date-fns` for dates, `react-day-picker` for calendars, `recharts` for charts, `qrcode` for QR codes, `papaparse` for CSV, and `@tiptap/react` for rich text. These names are starting points, not rules: check them with the questions below.
29
+ 3. **Your own code**, only when the job is small or nothing passes the checks.
30
+
31
+ For each candidate, answer these questions. If any answer is "no", drop it:
32
+
33
+ | Question | How to check |
34
+ |---|---|
35
+ | Does it run on Cloudflare Workers (server parts) or in the browser (client parts)? | No native modules, no `fs` or `child_process` at runtime, no long-lived servers. Browser-only libraries go in `"use client"` components. |
36
+ | Is it maintained? | A release in the last 12 months (`npm view <pkg> time.modified`), and real usage (weekly downloads). |
37
+ | Is the license usable? | MIT, Apache-2.0, BSD or ISC. Avoid GPL and AGPL in the merchant's app. |
38
+ | Is it small enough? | Check the bundle size (bundlephobia, or the build output). The whole Worker must stay well under 10 MB. Prefer libraries that tree-shake. |
39
+ | Does it work for the users' region? | For users in mainland China, avoid anything that loads from Google (Maps, Fonts, reCAPTCHA) or other blocked hosts at runtime. Use regional services instead, for example AMap or Tencent Maps for maps. |
40
+ | Does it support the language? | Chinese labels and locale for dates (`date-fns/locale/zh-CN`), numbers and calendars. |
41
+
42
+ ## 3. Spike it (10–15 minutes)
43
+
44
+ 1. Install only the chosen package.
45
+ 2. Build the smallest real use on a scratch page (for example `app/_spike/page.tsx`): the actual component with real data shapes, not a toy.
46
+ 3. Run `bun run build`. The build must pass and must not grow by more than you expected; compare the size of `dist/` before and after.
47
+ 4. If it has a server part, call it from a route handler and run `bun run dev` to see it work.
48
+ 5. If the user gave a deploy target, a spike can go live with `praxis app deploy`, but remove the scratch page before the real release.
49
+
50
+ If the spike fails, try the next candidate. **Two failed candidates means stop and tell the user** what you tried and what you suggest instead (a simpler feature, or your own code), rather than fighting the platform.
51
+
52
+ ## 4. Adopt and record
53
+
54
+ - Move the spike into the real feature and delete the scratch page.
55
+ - Add one line per adopted library under "## Libraries" in the project's `README.md`: name, what it is for, and why it was chosen ("react-day-picker: booking calendar; Workers-safe, 12 kB, zh-CN locale").
56
+ - In the report to the user, mention a library only when it affects them, for example a map provider that needs their own key.
@@ -0,0 +1,57 @@
1
+ # Praxis app
2
+
3
+ A Next.js App Router app (run by vinext on Cloudflare Workers) with end-user sign-up and sign-in,
4
+ a D1 database, R2 image upload and an admin area. It deploys through the Praxis Application API only.
5
+
6
+ ## Stack
7
+
8
+ - **vinext + Next.js 15 App Router**: ordinary `app/` pages and route handlers, built to a Worker.
9
+ - **D1 + drizzle-orm**: queries are typed; schema is plain SQL migrations applied at runtime.
10
+ - **R2**: image storage (binding `IMAGES`).
11
+ - **Auth**: username + password (PBKDF2-SHA256 via WebCrypto), HttpOnly signed session cookie, sessions stored hashed in D1. The first user to register becomes `admin`.
12
+ - **Tailwind CSS 3 + shadcn/ui-style primitives** in `components/ui/`.
13
+ - **zod** for input validation.
14
+
15
+ ## Folder map
16
+
17
+ ```
18
+ praxis.jsonc project file: kind, name, secrets, healthcheck (read by `praxis app deploy`)
19
+ wrangler.jsonc bindings the app needs (read at build time; never used to deploy)
20
+ app/ pages and route handlers (/, /login, /register, /account, /admin, /api/*, /images/*)
21
+ lib/ server-only code and business rules
22
+ db.ts getDb(): migrates once per isolate, returns the drizzle client
23
+ migrate.ts runtime migrator migrations.ts bundled migration list schema.ts drizzle tables
24
+ auth.ts password hashing, cookie signing, validation (no framework imports)
25
+ session.ts session rows + cookie strings users.ts user rules (first user = admin)
26
+ guard.ts getCurrentUser / requireUser / requireAdmin
27
+ storage.ts image rules and R2/D1 access
28
+ components/ui/ Button, Input, Label, Card, Badge
29
+ migrations/ numbered SQL files
30
+ ```
31
+
32
+ Keep business rules in `lib/`; pages and route handlers only call them.
33
+
34
+ ## Look
35
+
36
+ The look lives in the design tokens at the top of `app/globals.css` (colors, radius) and the font stack in `tailwind.config.ts`. Pages and components use only semantic classes (`bg-primary`, `text-muted-foreground`, `text-destructive`), never raw palette classes or hex colors, so restyling means editing the tokens. `bun test` enforces it: `lib/design-tokens.test.ts` fails on a raw color class and on any text/background token pair under 4.5:1 contrast.
37
+
38
+ ## Common changes
39
+
40
+ - **Add a page**: create `app/<name>/page.tsx`. Call `await requireUser("/<name>")` or `requireAdmin` from `lib/guard.ts` to protect it.
41
+ - **Add a table**: write `migrations/0002_<name>.sql` (idempotent: `CREATE TABLE IF NOT EXISTS`), import it with `?raw` and append it in `lib/migrations.ts`, then add the table to `lib/schema.ts`. It is applied on the first request after deploy.
42
+ - **Add a binding**: declare it in `wrangler.jsonc` (supported: `d1_databases`, `kv_namespaces`, `r2_buckets`, `vars`), add its type to `env.d.ts`, read it with `import { env } from "cloudflare:workers"`. The platform creates worker-owned resources and ignores ids.
43
+ - **Add a secret**: add it to `Cloudflare.Env` in `env.d.ts`, read `env.NAME`, and declare it in `praxis.jsonc` `secrets` (`"generate"` makes `praxis app deploy` create a random value once and keep it). To set your own value: `praxis api application worker secret put`. Secrets are never written in files. `SESSION_SECRET` is required: without it sign-in and sign-up return a 500 naming it.
44
+
45
+ ## Run
46
+
47
+ - `bun install` then `bun run dev` for local development.
48
+ - `bun run typecheck`, `bun test` (auth and migrator unit tests), `bun run build` (output in `dist/`).
49
+ - `praxis app deploy` builds, uploads and deploys, then checks `healthcheck`. `praxis app rollback` returns to the previous version. Nothing here calls wrangler to deploy.
50
+
51
+ ## Not supported on the platform
52
+
53
+ Service bindings, queues, Durable Objects, Workers AI, Analytics Engine. Use D1, KV, R2, plain/json/secret values and static assets.
54
+
55
+ ## Not included yet
56
+
57
+ Rate limiting (comments in `app/api/auth/*/route.ts` mark where it goes), password reset, email verification.
@@ -0,0 +1,7 @@
1
+ node_modules
2
+ dist
3
+ .wrangler
4
+ .dev.vars
5
+ .next
6
+ next-env.d.ts
7
+ bun.lock
@@ -0,0 +1,47 @@
1
+ import { Badge } from "@/components/ui/badge";
2
+ import { formatBytes } from "@/lib/format";
3
+ import { Button } from "@/components/ui/button";
4
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
5
+ import { Input } from "@/components/ui/input";
6
+ import { getDb } from "@/lib/db";
7
+ import { requireUser } from "@/lib/guard";
8
+ import { listImages } from "@/lib/storage";
9
+
10
+ export const dynamic = "force-dynamic";
11
+
12
+ export default async function Account({ searchParams }: { searchParams: Promise<{ error?: string }> }) {
13
+ const user = await requireUser("/account");
14
+ const { error } = await searchParams;
15
+ const mine = await listImages(await getDb(), user.id);
16
+ return (
17
+ <div className="space-y-6">
18
+ <Card>
19
+ <CardHeader><CardTitle>{user.username}</CardTitle></CardHeader>
20
+ <CardContent className="text-sm"><Badge variant={user.role === "admin" ? "default" : "secondary"}>{user.role}</Badge></CardContent>
21
+ </Card>
22
+ <Card>
23
+ <CardHeader><CardTitle>Upload an image</CardTitle></CardHeader>
24
+ <CardContent>
25
+ <form action="/api/images" method="post" encType="multipart/form-data" className="flex flex-col gap-3 sm:flex-row sm:items-center">
26
+ <Input name="image" type="file" accept="image/jpeg,image/png,image/webp" required />
27
+ <Button type="submit">Upload</Button>
28
+ </form>
29
+ {error && <p role="alert" className="mt-2 text-sm text-destructive">{error}</p>}
30
+ </CardContent>
31
+ </Card>
32
+ <Card>
33
+ <CardHeader><CardTitle>Your images ({mine.length})</CardTitle></CardHeader>
34
+ <CardContent>
35
+ <ul className="grid grid-cols-2 gap-3 sm:grid-cols-3">
36
+ {mine.map(img => (
37
+ <li key={img.id} className="text-xs">
38
+ <a href={`/images/${img.key}`}><img src={`/images/${img.key}`} alt="" className="h-32 w-full rounded-md border object-cover" /></a>
39
+ <span className="text-muted-foreground">{formatBytes(img.size)}</span>
40
+ </li>
41
+ ))}
42
+ </ul>
43
+ </CardContent>
44
+ </Card>
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,53 @@
1
+ import { count, desc } from "drizzle-orm";
2
+ import { Badge } from "@/components/ui/badge";
3
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
4
+ import { getDb } from "@/lib/db";
5
+ import { formatBytes, formatDateTime } from "@/lib/format";
6
+ import { requireAdmin } from "@/lib/guard";
7
+ import { images, users } from "@/lib/schema";
8
+
9
+ export const dynamic = "force-dynamic";
10
+
11
+ export default async function Admin() {
12
+ await requireAdmin("/admin");
13
+ const db = await getDb();
14
+ const [allUsers, allImages, [userCount], [imageCount]] = await Promise.all([
15
+ db.select({ id: users.id, username: users.username, role: users.role, createdAt: users.createdAt }).from(users).orderBy(users.createdAt),
16
+ db.select().from(images).orderBy(desc(images.createdAt)),
17
+ db.select({ n: count() }).from(users),
18
+ db.select({ n: count() }).from(images),
19
+ ]);
20
+ const nameOf = new Map(allUsers.map(u => [u.id, u.username]));
21
+ return (
22
+ <div className="space-y-6">
23
+ <Card>
24
+ <CardHeader><CardTitle>Users ({userCount!.n})</CardTitle></CardHeader>
25
+ <CardContent>
26
+ <div className="overflow-x-auto"><table className="w-full text-left text-sm [&_td]:py-2 [&_td]:pr-4 [&_th]:pb-2 [&_th]:pr-4 [&_th]:font-medium [&_th]:text-muted-foreground">
27
+ <thead><tr><th>Username</th><th>Role</th><th>Created</th></tr></thead>
28
+ <tbody>
29
+ {allUsers.map(u => (
30
+ <tr key={u.id}><td>{u.username}</td><td><Badge variant={u.role === "admin" ? "default" : "secondary"}>{u.role}</Badge></td><td className="whitespace-nowrap">{formatDateTime(u.createdAt)}</td></tr>
31
+ ))}
32
+ </tbody>
33
+ </table></div>
34
+ </CardContent>
35
+ </Card>
36
+ <Card>
37
+ <CardHeader><CardTitle>Images ({imageCount!.n})</CardTitle></CardHeader>
38
+ <CardContent>
39
+ {allImages.length === 0 ? (
40
+ <p className="text-sm text-muted-foreground">No uploads yet. Members add images from their account page.</p>
41
+ ) : <div className="overflow-x-auto"><table className="w-full text-left text-sm [&_td]:py-2 [&_td]:pr-4 [&_th]:pb-2 [&_th]:pr-4 [&_th]:font-medium [&_th]:text-muted-foreground">
42
+ <thead><tr><th>Owner</th><th>Image</th><th>Type</th><th>Size</th></tr></thead>
43
+ <tbody>
44
+ {allImages.map(i => (
45
+ <tr key={i.id}><td>{nameOf.get(i.ownerId) ?? i.ownerId}</td><td><a href={`/images/${i.key}`} aria-label={`Open image from ${nameOf.get(i.ownerId) ?? "a member"}`}><img src={`/images/${i.key}`} alt="" className="h-12 w-12 rounded-md border border-border object-cover" /></a></td><td>{i.contentType}</td><td className="whitespace-nowrap">{formatBytes(i.size)}</td></tr>
46
+ ))}
47
+ </tbody>
48
+ </table></div>}
49
+ </CardContent>
50
+ </Card>
51
+ </div>
52
+ );
53
+ }
@@ -0,0 +1,30 @@
1
+ import { env } from "cloudflare:workers";
2
+ import { credentialsSchema, hashPassword, MissingSecretError, requireSecret, verifyPassword } from "@/lib/auth";
3
+ import { getDb } from "@/lib/db";
4
+ import { redirectResponse, safeNext } from "@/lib/guard";
5
+ import { createSession, sessionCookie } from "@/lib/session";
6
+ import { findUserByUsername } from "@/lib/users";
7
+
8
+ // Rate limiting for sign-in (per IP and per username) would go here, before the password check.
9
+ export async function POST(req: Request) {
10
+ try {
11
+ requireSecret(env.SESSION_SECRET);
12
+ const form = await req.formData();
13
+ const next = String(form.get("next") ?? "");
14
+ const fail = () => redirectResponse(`/login?error=${encodeURIComponent("Wrong username or password")}${next ? `&next=${encodeURIComponent(next)}` : ""}`);
15
+ const parsed = credentialsSchema.safeParse({ username: form.get("username"), password: form.get("password") });
16
+ if (!parsed.success) return fail();
17
+ const db = await getDb();
18
+ const user = await findUserByUsername(db, parsed.data.username);
19
+ if (!user) {
20
+ await hashPassword(parsed.data.password); // spend the same time as a real check
21
+ return fail();
22
+ }
23
+ if (!(await verifyPassword(parsed.data.password, user.passwordHash))) return fail();
24
+ const session = await createSession(db, user.id, env.SESSION_SECRET);
25
+ return redirectResponse(safeNext(next), sessionCookie(session.value, session.expiresAt));
26
+ } catch (e) {
27
+ if (e instanceof MissingSecretError) return new Response(e.message, { status: 500 });
28
+ throw e;
29
+ }
30
+ }