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 save action might run logic in workflow/save.handler.ts AND analytics/save.handler.ts simultaneously.

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 dispatch and select. 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]Provider for single items and [PluralObject]Provider for lists (e.g., RideProvider vs RidesProvider).
  • 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.
  • 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.