Newtown Developer Manual
Comprehensive guides and references for developers building on the Newtown platform.
Need to understand the user experience? Check out the User Manual for application workflows.
Core Concepts
A group can build and run multiple apps in Newtown. Each app is organized into blocks and pages.
Blocks
Blocks are the core functional units of an app. Each block represents a capability such as identity, messaging, tasks, or auctions. Blocks keep related logic and data together so they remain modular and reusable.
A block typically contains:
- Fields – The pieces of data stored for an item (for example: status, title, due date).
- Actions – Operations that change data (create, update, delete, or custom behaviors).
- Components – UI triggers that initiate actions (such as buttons or controls).
Blocks operate independently, which keeps functionality clean and maintainable.
Coordination
Coordination handles side effects between blocks. When something changes in one block, coordination defines what should happen in others. This keeps blocks independent while still allowing apps to respond to events.
Pages
Pages bring blocks together into a usable interface. A page organizes forms, lists, and views so members of the group can interact with the app.
While blocks define how things work, pages define how people use them.
Fields
Fields are state representations of object properties integrated with data providers on React components. They consume context from an Object Provider and handle their own display/edit states.
Field Components
Syntax
import { useUser } from "@/providers/user";
import { Input } from "@/components/ui/input";
export const UserName = ({ edit, className }) => {
const { user, form } = useUser();
// Using form.props binding for edit mode
return edit
? (<Input {...form.props("name")} className={className} />)
: <span className={className}>{user.name}</span>;
};Association Fields
Associations manage relationships between objects using nested providers.
One-to-Many Example
import { CommentsProvider } from "@/providers/comment";
// Inside PostProvider
<CommentsProvider ids={post.comment_ids}>
<CommentsProvider.Item>
{(comment) => <CommentContent />}
</CommentsProvider.Item>
</CommentsProvider>Actions
Actions are stateless mutation handlers that execute custom business logic on the server. They perform CRUD operations and complex logic on blocks, adhering to strict isolation rules.
Syntax
Action handlers are exported as handler functions.
// objects/task/workflow/actions/create.handler.ts
export const handler = async ({ state }, payload) => {
// Validate payload
if (!payload.title) throw new Error("Title is required");
// Mutate state (create/update)
// Note: ID must be passed explicitly in payload for creation
return await state.set(payload);
};API
Handler Arguments The handler receives a context object and a payload.
| Property | Type | Description |
|---|---|---|
state |
object |
Interface to get/set/delete the current block's state. |
select |
function |
API to query other objects (read-only). |
dispatch |
function |
API to trigger actions on other blocks/objects. |
State Methods
The state object provides methods to manage the block's data.
| Method | Description |
|---|---|
state.get(id) |
Fetches an object by ID. |
state.set(data) |
Creates or updates an object. Merges with existing state. |
state.delete(id) |
Deletes an object by ID. |
Defaults (Built-in Actions)
Every object comes with three built-in actions:
- create: Creates a new record. Payload must include
id. - update: Updates an existing record. Merges payload with existing state.
- delete: Removes a record by
id.
Mutation Hooks
To trigger actions from the frontend, use the hooks exposed by the generated actions object from the provider.
Built-in Mutations
const { actions } = useTodo();
const { useCreateMutation, useUpdateMutation, useDeleteMutation } = actions;
// Triggering a create action
const create = useCreateMutation();
create.mutate({ id: "todo_1", title: "Buy Milk" });Custom Action Mutations
For a custom action named mark_done, the hook name is camel-cased (useMarkDoneMutation).
const { useMarkDoneMutation } = actions;
const markDone = useMarkDoneMutation();
markDone.mutate({ id: todo.id });Coordination
Coordination manages side effects and cross-block interactions. It ensures that an action in one block can trigger updates in others without tight coupling.
Patterns
1. Parallel Block Updates (Same Object) If multiple blocks on the same object define an action with the same name, they run in parallel.
- Example: A
saveaction might run logic inworkflow/save.handler.tsANDanalytics/save.handler.tssimultaneously.
2. Cross-Object Dispatch To update a different object, use a Coordination Block.
- This block listens for the action and dispatches a new action to the target object.
- Convention:
[action_name]_[target_object_snake_case]
// objects/task/coordination/actions/assign.handler.ts
export const handler = async ({ dispatch }, payload) => {
// Dispatch 'notify_assignment' action to the 'user' object
return await dispatch("notify_assignment_user", {
id: payload.assignee_id,
task_id: payload.id
});
};API
dispatch(action_name, payload) Triggers an action on a target object.
- Returns the result of the dispatched action.
select(object_name, query) Fetches data from other objects (read-only) for use in coordination logic.
const [user] = await select('user', { id: payload.user_id });Rules
- Isolation: Coordination handlers should only use
dispatchandselect. They should not mutate local state directly (use the parallel block pattern for that). - Placement: Coordination handlers must exist alongside other blocks for the same object; they are never standalone.
Components
In Malleable, Components specifically refer to Trigger Components and Association Displays. They are purely functional units used to trigger logic or display related data, distinct from Pages or general UI layouts.
1. Trigger Components
These components are designed to execute actions. They consume the context from a parent Provider (Object or Collection) to access actions and form state.
Key Rule: Do not define new providers or forms inside a trigger component. Use the context passed down from the Page.
Example: Create Button
import { Button } from "@/components/ui/button";
import { useTask } from "@/providers/task"; // Consuming context
export const CreateTaskButton = ({ onSuccess }) => {
// 1. Access actions and form state from the provider
const { actions, form } = useTask();
// 2. Select the specific mutation
const { mutateAsync: createMutate, isPending } = actions.useCreateMutation();
const handleCreate = async () => {
// 3. Execute mutation with current form state
const { data } = await createMutate(form.state);
if (data?.id) {
form.reset();
if (onSuccess) onSuccess(data);
}
};
return (
<Button onClick={handleCreate} disabled={isPending}>
Create Task
</Button>
);
};Available Mutations
actions.useCreateMutation()actions.useUpdateMutation()actions.useDeleteMutation()actions.use[ActionName]Mutation()(for custom actions)
2. Association Components
These components handle displaying related data. They manage the Provider nesting required to fetch and render associated objects.
One-to-One Association
import { UserProvider } from "@/providers/user";
import { UserAvatar } from "@/components/user/UserAvatar";
export const TaskAssignee = () => {
const { task } = useTask();
if (!task.assignee_id) return <span>Unassigned</span>;
// Nest the related provider using the foreign key
return (
<UserProvider id={task.assignee_id}>
<UserAvatar />
</UserProvider>
);
};One-to-Many Association
import { CommentsProvider } from "@/providers/comments"; // Note plural provider
export const TaskComments = () => {
const { task } = useTask();
if (!task.comment_ids?.length) return null;
return (
<CommentsProvider ids={task.comment_ids}>
<CommentsProvider.Item>
<CommentCard />
</CommentsProvider.Item>
</CommentsProvider>
);
};Best Practices
- Granularity: Keep trigger components small and focused on a single action.
- Naming: Use
[Object]Providerfor single items and[PluralObject]Providerfor lists (e.g.,RideProvidervsRidesProvider). - Separation: Do not mix form definition (Page responsibility) with action triggering (Component responsibility).
Object Provider
The Object Provider manages a single object instance, providing data, CRUD operations, and form state integration.
Typical Usage
<UserProvider id={"user_123"}>
<UserName />
<UserEmail edit />
<UpdateUserButton />
</UserProvider>useObject Hook
The core hook for accessing object context.
const { user, actions, form, loading, error } = useUser();| Property | Type | Description |
|---|---|---|
object_name |
object |
The current object data (e.g., user, task). |
actions |
object |
Mutations for CRUD and custom actions. |
form |
object |
Form state management helpers. |
loading |
boolean |
Whether the object data is currently loading. |
error |
object |
Any error that occurred during fetch or mutation. |
Actions
The actions object exposes mutation hooks.
const { useUpdateMutation, useDeleteMutation } = actions;
const updateMutation = useUpdateMutation();
// Usage
updateMutation.mutate({ name: "New Name" });Form Management
The provider includes built-in form handling that syncs with the object state.
Form Helper Methods
| Method | Description |
|---|---|
form.props(field_name) |
Returns props (value, onChange) for binding to inputs. |
form.handleSubmit(callback) |
Wrapper for submission that handles validation. |
form.reset() |
Resets the form to the object's initial state. |
form.setValue(name, value) |
Manually sets a field value. |
Example: Binding to an Input
<Input {...form.props("email")} placeholder="Enter email" />Collections Provider
The Collections Provider manages lists of objects, handling filtering, sorting, and pagination efficiently.
Typical Usage
<TasksProvider
filters={{ status: "pending" }}
sort={{ created_at: "desc" }}
>
<TasksProvider.Item>
{(task) => <TaskCard key={task.id} />}
</TasksProvider.Item>
</TasksProvider>Provider Props
| Prop | Type | Description |
|---|---|---|
filters |
object |
Key-value pairs for filtering. Multiple keys are combined with AND. Array values are combined with OR. |
sort |
object |
Sort configuration (e.g., { due_date: "asc" }). |
ids |
array |
(Optional) Specific list of IDs to fetch. |
Advanced Filter Examples
Multiple filters act as an AND condition.
// Fetch tasks that are both "pending" AND assigned to "user_123"
filters={{
status: "pending",
assignee_id: "user_123"
}}Array values act as an OR condition (IN clause).
// Fetch tasks where status is "pending" OR "in_progress"
filters={{
status: ["pending", "in_progress"]
}}Hooks
These hooks must be used within a Collections Provider.
useResults()
Returns the array of fetched objects.
const { data: tasks, loading, error } = useResults();
if (loading) return <Spinner />;
return <div>{tasks.length} tasks found</div>;useFirst()
Returns the first item from the results array. Useful for singleton-like access in a list context.
const { data: firstTask } = useFirst();usePage()
Manages pagination state (offset and limit).
const [{ offset, limit }, setPage] = usePage();
// reliable pagination
const nextPage = () => setPage(offset + limit, limit);useSort()
Manages sorting state for a specific field.
const [direction, setSort] = useSort("created_at");
// direction is "asc" or "desc"
<button onClick={() => setSort(direction === "asc" ? "desc" : "asc")}>
Sort by Date
</button>Pages & Composition
Pages assemble blocks (fields, actions, providers) into cohesive user interfaces. A page is responsible for layout, routing, and composing different capabilities.
Index & Routing
The index file defines the application shell and routing. Navigation is managed via the apps array.
const apps = [
{
name: "Tasks App",
href: "/tasks", // Route path
description: "Manage your daily to-dos",
component: TasksPage // Imported page component
}
];Composition Patterns
1. Create/Update Forms Combine an Object Provider, Fields (in edit mode), and a Create/Update Button.
import { TaskProvider, CreateTaskButton } from "@/objects/task";
// Note: Import components from '@/objects/<name>' for composition.
export const CreateTaskForm = () => {
return (
<TaskProvider>
<div className="space-y-4">
{/* Fields handle their own form binding */}
<TaskTitle edit />
<TaskDescription edit />
{/* Built-in button handles submission */}
<CreateTaskButton onSuccess={() => alert("Created!")}>
Add Task
</CreateTaskButton>
</div>
</TaskProvider>
);
};2. List Views Use a Collections Provider to iterate over items.
import { TasksProvider } from "@/objects/task";
export const TaskList = () => {
return (
<TasksProvider sort={{ created_at: "desc" }}>
<div className="grid gap-4">
<TasksProvider.Item>
{/* Function-as-child pattern gives access to individual item context */}
<TaskCard />
</TasksProvider.Item>
</div>
</TasksProvider>
);
};3. Detailed Views
Use an Object Provider with a specific id.
export const TaskDetail = ({ taskId }) => {
return (
<TaskProvider id={taskId}>
<h1><TaskTitle /></h1>
<p><TaskDescription /></p>
<UpdateTaskToggle />
</TaskProvider>
);
};Rules
- Flat Structure: Do not nest page files. Declare all page components in the same file if possible.
- Imports:
- Use
@/objects/<name>for UI components (Buttons, Fields) and Providers when composing pages. - Use
@/providers/<name>only when you need the raw hooks (use<Object>) for custom logic.
- Use
- Auth: Use
useAuthandAuthProviderfor managing session state.
Authentication
Authentication in Newtown wraps the entire application shell, providing session management and passkey support.
Setup
The AuthProvider must wrap the router in your index file.
// index.tsx
import { AuthProvider } from "@/utils/auth";
import { BrowserRouter as Router } from "react-router-dom";
export function Index() {
return (
<AuthProvider>
<Router>
<AppShell />
</Router>
</AuthProvider>
);
}useAuth Hook
Access the current user and authentication methods.
const { user, loginPasskey, registerPasskey, logout } = useAuth();
// Check if user is logged in
if (user) {
console.log("Logged in as:", user.username);
}Login Flow
To log in an existing user, use loginPasskey.
const Login = () => {
const { loginPasskey } = useAuth();
const [username, setUsername] = useState("");
const handleLogin = async () => {
// Initiates passkey authentication flow
await loginPasskey(username);
};
return (
<div>
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Username"
/>
<button onClick={handleLogin}>Login with Passkey</button>
</div>
);
};Registration Flow
Registration is a two-step process:
- Create the user record using
UserProviderandCreateUserButton. - Register the passkey for the new user using
registerPasskey.
import { UserProvider, CreateUserButton, UserUsername } from "@/objects/user";
const Register = () => {
const { registerPasskey } = useAuth();
return (
<UserProvider>
{/* 1. User inputs their desired username */}
<UserUsername edit placeholder="Choose a username" />
{/* 2. Create the user record */}
<CreateUserButton
onSuccess={async (newUser) => {
// 3. Register passkey immediately after creation
await registerPasskey(newUser.id, newUser.username);
}}
>
Sign Up
</CreateUserButton>
</UserProvider>
);
};Integrations & Utilities
Newtown provides utility helpers for common tasks like file handling, email communication, and using external packages.
File Uploads
Use the uploadFile and deleteFile helpers from utils.
Upload Flow
- Select File: User picks a file.
- Upload: Call
uploadFile(file_obj)to get a public URL. - Save URL: Store the returned URL in your object's field.
import { uploadFile } from "@/utils";
const handleFileChange = async (event) => {
const file = event.target.files[0];
if (!file) return;
try {
const publicUrl = await uploadFile(file);
// Update your object field with this URL
form.handleChange("avatar_url", publicUrl);
} catch (error) {
console.error("Upload failed", error);
}
};Delete Flow Always delete the old file when replacing or removing it.
import { deleteFile } from "@/utils";
// When removing an avatar
await deleteFile(currentAvatarUrl);
form.handleChange("avatar_url", null);Send emails from Action Handlers (server-side only) using the sendEmail helper.
// objects/order/notifications/actions/send_receipt.handler.ts
import { sendEmail } from "utils";
export const handler = async ({ state }, payload) => {
await sendEmail({
to: [payload.customer_email],
subject: "Your Order Receipt",
text: `Thank you for your order #${payload.order_id}!`,
// Optional HTML body
html: `<p>Thank you for your order <strong>#${payload.order_id}</strong>!</p>`
});
};Third-Party Packages
You can import external libraries directly using ESM syntax (Deno/browser compatible).
Format: https://esm.sh/<package_name>@<version>
Example: Using Date-fns
import { formatDistanceToNow } from "https://esm.sh/date-fns@2.30.0";
export const TimeAgo = ({ date }) => {
return <span>{formatDistanceToNow(new Date(date))} ago</span>;
};Example: Using Lodash
import { debounce } from "https://esm.sh/lodash@4.17.21";
const debouncedSearch = debounce((query) => {
// perform search
}, 300);Import Rules
Understanding how to import dependencies is critical for maintaining a scalable codebase. Supports specific path imports and barrel imports for module patterns.
Path Aliases
| Alias | Description | Example |
|---|---|---|
@/components/ui/* |
Base UI components (shadcn/ui). | import { Button } from "@/components/ui/button" |
@/providers/{object} |
Generated Providers & Hooks. | import { useUser } from "@/providers/user" |
@/objects/{object} |
High-level Object Exports (Barrel). | import { UserCard } from "@/objects/user" |
@/objects/{obj}/{block}/... |
Specific Field/Component implementation. | import { Title } from "@/objects/todo/main/fields/title" |
1. In Components & Fields (Low-Level)
When building individual Fields or Trigger Components, use Specific Imports. This avoids circular dependencies and keeps bundles lean.
UI Components: Import directly from
@/components/ui.import { Button } from "@/components/ui/button";Providers: Import strictly from the provider path.
// ✅ Correct import { useTask } from "@/providers/task";Specific Peers: If you need a sibling component, import it by its full path.
import { UserAvatar } from "@/objects/user/profile/components/avatar";
2. In Pages (High-Level)
When constructing Pages or Cards, use Barrel Imports. The framework generates index files for each object that export all its public fields, components, and providers.
Object-Level Imports: Bring in everything you need for a specific object from one place.
// ✅ Recommended for Pages import { TaskProvider, CreateTaskButton, TaskTitle, TaskStatus } from "@/objects/task";Rule: Do not use deep path imports (like
.../fields/title) in Pages. Rely on the exposed public API from the object barrel file.
Anti-Patterns
- ❌ Global Provider Import: defined as
import { useUser, useTask } from "@/providers"is not supported.- Fix: Import from
@/providers/userand@/providers/taskseparately.
- Fix: Import from
- ❌ Circular imports: Importing a Page into a Component, or importing the Object Barrel into an internal Field of that same object.
Looking for user-facing documentation? The User Manual explains how to prompt and use the apps.