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
In Newtown, your app is built from visual values — every piece of state, every behavior, is a React component you can see and compose.
Fields
Fields are state and associations as React components. A DueDate field renders a date picker in edit mode and a formatted date in display mode. An Assignee field renders the assigned user by nesting their provider.
"Add a title to posts" → you get a
Titlecomponent that displays the title or renders an input when editing.
Actions
Actions are behaviors as React components — a button, toggle, or control paired with a server-side handler. The component and the behavior are one thing.
"Add a vote button" → you get an
UpvoteButtoncomponent wired to a handler that increments the post score.
Blocks
A block is a named scope that groups related fields and actions around a single concern — voting, karma, moderation. Each block has its own isolated state — blocks cannot directly read or write each other's state. Blocks are polymorphic: multiple blocks on the same object can define the same action, each handling it from their own perspective, all firing in parallel.
"Add a karma block to users" → a new independent scope that tracks a user's karma score, with its own state, completely separate from identity or any other block.
Coordination
A coordination block is a special type of block whose actions dispatch to other objects instead of mutating local state. It sits alongside regular blocks on the same object and fires in parallel with them — but its job is cross-block and cross-object wiring, not local mutation.
"When a post is voted, update the user karma" → the
votingblock updates the post score, and thecoordinationblock on the same post simultaneously dispatches a karma update to the author — neither block knows about the other.
Objects
An object is the union of its blocks — the full thing you interact with through a provider. Remove a block and the rest still works.
Pages
Pages compose fields and actions into interfaces — forms, lists, detail views. While blocks define how things work, pages define how people use them.
"Build a hacker news style app" → posts with titles and scores, vote and comment buttons, karma on users, all composed into a feed and detail view.
Fields
Fields are state and associations as React components. A field knows how to display itself and how to edit itself — pass edit to switch modes. It reads from the provider context and writes through the form.
// displays the title
<TaskTitle />
// renders an input bound to the form
<TaskTitle edit />
// nests a UserProvider to display the assigned user
<TaskAssignee />Syntax
import { useUser } from "@/providers/user";
import { Input } from "@/components/ui/input";
export const UserName = ({ edit, className }) => {
const { user, form } = useUser();
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-One Association
import { UserProvider } from "@/providers/user";
export const TaskAssignee = ({ className }) => {
const { task } = useTask();
if (!task.assignee_id) return <span className={className}>Unassigned</span>;
return (
<UserProvider id={task.assignee_id}>
<UserName className={className} />
</UserProvider>
);
};One-to-Many Association
import { CommentsProvider } from "@/providers/comment";
export const PostComments = () => {
const { post } = usePost();
return (
<CommentsProvider ids={post.comment_ids}>
<CommentsProvider.Item>
{(comment) => <CommentContent />}
</CommentsProvider.Item>
</CommentsProvider>
);
};Actions
Actions are behaviors as React components. Each action is two things unified: a component (the trigger — a button, toggle, or control) and a server-side handler that runs when it fires.
<CompleteTaskButton /> // marks a task complete
<PlaceBidButton /> // places a bid on a listing
<AssignTaskButton /> // assigns a task to a teammateHandler Syntax
// objects/task/workflow/actions/complete_task.handler.js
export const handler = async ({ state }, payload) => {
return await state.set({ id: payload.id, status: "completed" });
};Handler Arguments
| Property | Type | Description |
|---|---|---|
state |
object |
Interface to get/set/delete the current block's state. |
select |
function |
Query other objects (read-only). Only in coordination blocks. |
dispatch |
function |
Trigger actions on other objects. Only in coordination blocks. |
State Methods
| 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. |
Built-in Actions
Every object comes with three built-in actions — no handler needed unless you want to override:
- create: Creates a new record.
- update: Updates an existing record.
- delete: Removes a record by
id.
Component Syntax
Action components consume context from a parent provider to access actions and form state. Do not define new providers or forms inside an action component — use what's passed down from the page.
import { Button } from "@/components/ui/button";
import { useTask } from "@/providers/task";
export const CreateTaskButton = ({ onSuccess }) => {
const { actions, form } = useTask();
const { mutateAsync: createMutate, isPending } = actions.useCreateMutation();
const handleCreate = async () => {
const { data } = await createMutate(form.state);
if (data?.id) {
form.reset();
if (onSuccess) onSuccess(data);
}
};
return (
<Button onClick={handleCreate} disabled={isPending}>
Create Task
</Button>
);
};Mutation Hooks
const { actions } = useTask();
// Built-in
const create = actions.useCreateMutation();
create.mutate({ title: "New Task" });
// Custom — action name is camel-cased
const completeTask = actions.useCompleteTaskMutation();
completeTask.mutate({ id: task.id });Coordination
A coordination block is a special type of block for cross-block and cross-object dispatch. Regular blocks mutate their own state. A coordination block never touches local state — it only reads from other objects with select and triggers actions on them with dispatch.
Blocks are polymorphic — when an action fires on an object, every block with a matching handler runs in parallel. Coordination slots in alongside them: same action name, same moment, different job.
Example: voting on a post
When upvote fires on a post, two blocks run simultaneously:
votingblock: increments the post's score (local state mutation)coordinationblock: reads the post's author and dispatches a karma update to the user object
// objects/post/voting/actions/upvote.handler.js
export const handler = async ({ state }, payload) => {
const [post] = await state.get(payload.id);
return await state.set({ id: payload.id, score: post.score + 1 });
};// objects/post/coordination/actions/upvote.handler.js
export const handler = async ({ select, dispatch }, payload) => {
const [post] = await select('post', { id: payload.id });
await dispatch("add_karma_user", {
id: post.author_id,
amount: 1
});
};// objects/user/karma/actions/add_karma.handler.js
export const handler = async ({ state }, payload) => {
const [user] = await state.get(payload.id);
return await state.set({ id: payload.id, karma: user.karma + payload.amount });
};The post object knows nothing about user karma. The user object knows nothing about voting. Coordination wires them without coupling either.
API
dispatch(action_name, payload)
Triggers an action on a target object. Convention: [action_name]_[object_name].
await dispatch("add_karma_user", { id: user_id, amount: 1 });select(object_name, query) Fetches data from another object (read-only).
const [post] = await select('post', { id: payload.post_id });Rules
- Coordination handlers only use
dispatchandselect— neverstate. - A
coordinationblock always coexists with other blocks; never standalone. - Dispatch convention:
[action_name]_[object_name](e.g.add_karma_user,notify_assignment_user).
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.