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 Title component 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 UpvoteButton component 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 voting block updates the post score, and the coordination block 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 teammate

Handler 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:

  • voting block: increments the post's score (local state mutation)
  • coordination block: 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 dispatch and select — never state.
  • A coordination block 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.
  • Auth: Use useAuth and AuthProvider for 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:

  1. Create the user record using UserProvider and CreateUserButton.
  2. 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

  1. Select File: User picks a file.
  2. Upload: Call uploadFile(file_obj) to get a public URL.
  3. 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);

Email

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/user and @/providers/task separately.
  • 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.