YouBothAgent▾
You — Business rules and flows you own. Read these yourself.
Both — Know the idea; your agent follows the details.
Agent — Conventions and references your agent follows. Look up as needed.
General▾
Interface▾
Observability▾
CRUD With Less Code
CRUD is usually the first screen you build: list records, open one, create one, edit it and remove it. In Akan the store actions and components for all five already exist, so you write the three pieces below and the pages that place them.
What you writeDescription
post.signal.ts
The slice. It decides which records this screen can read and edit.
Post.Template.tsx
Draws the form fields. Create and edit use the same one.
Post.Zone.tsx
Connects the slice to UI behaviour with the
Load and Model components.Each task maps to the components and generated store actions that do it:
| Task | Store key or action |
|---|---|
| ↳ Component | |
| List | postListInAdmin |
| Load.Units | |
| Open | viewPost(id) |
| Model.ViewWrapper + Model.ViewEditModal | |
| Create | newPost → submitPost |
| Load.Edit · Model.New | |
| Edit | editPost(id) → submitPost |
| Load.Edit · Model.Edit | |
| Remove | removePost(id) |
| Model.Remove | |
Start With A Slice
A slice is a named window into your model: the list one screen shows. Give it a name that matches the screen, such as
inPublic, inAdmin or inProject.1. Declare the filter
Say what "public" means in the document. Each filter adds a
query<Name>() method to the service:apps/blog/lib/post/post.document.ts
2. Expose it as a slice
Then list it in the signal, one key per screen:
apps/blog/lib/post/post.signal.ts
queryInPublic()comes from the filter.queryAny()comes from theanyfilter every model has.- Each key names the fetch methods.
inAdmingivesfetch.initPostInAdmin()andfetch.slice.postInAdmin, which the next steps use. - Return the query, do not shape it. Order and page size are fetch options (
{ sort, page, limit }), not.sort()on the query.
Who may call what
The
guards map covers the generated endpoints, and each named slice brings its own:KeyWhat it guards
root
The root slice
initPost(queryKey, args), which can run any filter, so it is always Admin.get
The single-record read
fetch.post(id), which viewPost and editPost also use.cru
createPost, updatePost, removePost; the create, update, remove keys override one each.init({ guards })
A named slice's own list, which the
guards map above never reaches.

A named slice is guarded only by its own
init({ guards }). get and cru never reach it, so inAdmin: init() with no guards serves every post to any caller. None closes an endpoint to everyone.List And Open
The page loads the first rows on the server. A Zone draws them with
Load.Units, and Model.ViewEditModal beside the list handles the detail view and the edit form.1. Load in the page
The page starts the slice query and hands the promise to the Zone:
apps/blog/page/admin/post/_index.tsx
2. Draw it in a Zone
Each card opens its post, and one modal shows whichever post is open:
apps/blog/lib/post/Post.Zone.tsx
- Why a Zone.
renderItem,renderViewandrenderTemplateare functions, and a server page cannot pass a function to a client component. - One modal serves every card.
Model.ViewWrappercallsst.do.viewPost(id), and the modal drawsrenderView. - Edit is built in. The modal's Edit button swaps in
renderTemplate, and Save returns to the view. The ⋮ menu holds Remove;menu={false}hides it. - No empty or paging code.
Load.Unitsshows<Empty />when there are no rows and paginates by default.
Create And Edit
Use the same Template for create and edit. The shell, the component around the form, prepares
postForm and saves it, so the Template only cares about fields. A form with no id is created, one with an id is updated.Pick the shell by where the form should appear:
ShellWhere it goes and what opens it
Load.Edit
Goes on a page of its own, such as
new.tsx; the form is open as soon as the route is.Model.New
Goes anywhere and draws its own New button; a click calls
st.do.newPost().Model.Edit
Goes anywhere and draws its own Edit button; a click calls
st.do.editPost(id).Model.ViewEditModal
Goes beside a list in a Zone; the Edit button in its detail view opens the form.
Create page
A create page seeds the form with the values a new post starts with:
apps/blog/page/admin/post/new.tsx
Edit page
An edit page loads the record on the server and hands it to the same shell:
apps/blog/page/admin/post/[postId]/edit.tsx
type="form"draws the form in place with its own Save button. The default,"modal", opens it in a modal.slicenames the list a new post joins. A create throughpostInAdminlands at the top ofpostListInAdmin; an edit updates every list already loaded.onSubmitandonCanceltake a path."back"goes back, and[postId]in anonSubmitpath becomes the saved post's id.- The form survives accidents. The shell saves the form as the user types and offers it back on the next open.
draft={false}turns that off.
Edit in a modal
To edit without leaving the screen,
Model.Edit draws an Edit button and its modal together:apps/blog/lib/post/Post.Util.tsx
- A click loads the record. It calls
st.do.editPost(postId), which fillspostFormand opens the modal. - Button and modal in different places?
Model.EditisModel.EditWrapper(the trigger) plusModel.EditModal id={postId}(the modal). The modal alone opens nothing untileditPostruns. Model.Newis the create twin. Same props minusmodelId;partialseeds the new form.
Remove In Util
Delete buttons usually appear in many places: a card, a detail view, a menu. Put one
Remove in Post.Util.tsx, next to Edit, so the Unit, View and Zone files stay simple:apps/blog/lib/post/Post.Util.tsx
- It asks first. A click opens a confirm modal; confirming calls
st.do.removePost(postId)and shows a success toast. - Every list drops the row. Removal goes by id, so any slice of the model works here;
fetch.slice.postis the root one. - On a detail page,
redirect="back"or a path moves away after the removal.namefills the confirm sentence, andtitle,description,actionreplace parts of the modal.
Tips
- Name slices after screens, not database queries.
inAdminsays who looks at the list, not how it is fetched. - Keep the Template boring. It reads
postFormand draws fields, with each setter passed by reference:onChange={st.do.setTitleOnPost}. - Repeated actions go to Util. Remove, publish, approve or open a dialog: write it once in
Post.Util.tsxand place it anywhere.
Read next
Forms→
The Template, the create and edit pages, and every shell option in detail.
model.signal.ts→
Slices, guards and the fetch names each slice key produces.
model.Zone.tsx→
How a Zone hydrates the store and hands rows to Unit and View.
model.Util.tsx→
One domain action as a control, such as
Remove or Publish.