Building app features
Add a page under /app with server-rendered partials, Datastar interactivity and activity logging.
The dashboard is server-rendered Astro. Datastar swaps HTML fragments into the page, so there's no client-side data layer to keep in sync. The Notes app in src/pages/app/notes/ is the reference to copy.
Add the table
Define it in src/db/schema.ts with an organizationId, then generate and apply a migration.
Add the page
Create src/pages/app/<feature>/index.astro. Check for a user, redirect to /login if there isn't one, and render it inside the App layout.
Add partials
Create small .astro pages for the list and the form. They return an HTML fragment with no layout.
Add the mutation endpoint
Handle the form POST in an actions.astro page or a .ts route. Validate with Zod and scope the write to the team.
Log the activity
Call logActivity() for anything the user would want in their history.
Link it in the sidebar
Add an entry to the sideLinks array in src/layouts/App.astro.
Partials
A partial is a normal .astro page with no layout. Two response headers tell Datastar where to put it:
---
export const prerender = false;
if (!Astro.locals.user) return new Response('Unauthorized', { status: 401 });
Astro.response.headers.set('datastar-selector', '#project-modal-content');
Astro.response.headers.set('datastar-mode', 'inner');
---
<div>
<form method="POST" action="/app/projects/save">…</form>
</div>Load it from the page with @get:
<button data-on:click="$showModal = true; @get('/app/projects/form?id=42')">Edit</button>Give each partial a single root element. Render the same component from the full page and the partial so the first load and later refreshes match.
Forms that work without JavaScript
Use a real form with method="POST" and an action, then enhance it:
<form method="POST" action="/app/projects/save"
data-on:submit__prevent="@post('/app/projects/save', { contentType: 'form' })">The endpoint checks isDatastarRequest() and either streams an update or redirects:
import { datastarSSE, isDatastarRequest, sseRedirect } from '@/lib/datastar';
const ds = isDatastarRequest(ctx.request);
if (!ctx.locals.user) return ds ? sseRedirect('/login') : ctx.redirect('/login');
// validate, write, logActivity…
return ds
? datastarSSE().patchElements(html, { selector: '#projects-list', mode: 'inner' }).toast('Saved', 'success').response()
: ctx.redirect('/app/projects');Astro actions
Account and team changes use Astro actions in src/actions/. They validate input with Zod and can be called from a form's action attribute. Use them when a typed function call fits better than an endpoint.
Activity logging
import { ActivityType, logActivity } from '@/lib/auth.server';
await logActivity(Astro.locals.organizationId, Astro.locals.user.id, ActivityType.UPDATE_ACCOUNT);Add new values to the ActivityType enum in src/db/schema.ts. The column is text, so a new value doesn't need a migration.
UI components
src/components/daisy-ui/ has buttons, inputs, cards, dialogs, tables, tabs, toasts and more, built on daisyUI 5. Import from each folder, for example import { Button } from "@/components/daisy-ui/button". Plain daisyUI classes such as btn and modal work too.