A real-time chat app with AI agents built with SolidStart, PowerSync, Neon, and Mastra.
- 💬 Real-time chat channels
- 🤖 AI agents you can @mention in channels
- ✨ Agent runs are triggered directly from message upload handling
- 👥 Channel sidebar - view members and agents
- 👤 Agent viewer - click agents to view their description and instructions
- 📱 Offline-first with PowerSync local-first sync
- ⚡ Instant UI updates via client-side writes
- 🔐 Anonymous sessions (no signup required for MVP)
- Frontend: SolidStart + Solid Router
- Database: Neon Postgres
- Sync: PowerSync (Web SDK + Service)
- AI: Mastra with OpenAI GPT-5
- Auth: Anonymous cookie-based sessions
bun installThis app expects a Postgres database with the chat schema already created.
- Tables used by the app:
users,agents,channels,channel_members,messages,agent_runs - Seed/demo agents commonly used in development:
Assistant,Analyst,Researcher,Writer - Schema source:
src/db/schema/server.ts - Migrations:
db/migrationsgenerated by Drizzle viabun run db:generate
Set NEON_DATABASE_URL in .env.local (see Environment Variables below).
PowerSync setup is split across a few files:
db/replication.sql- SQL for the Postgres replication role and publicationpowersync/service.yaml- PowerSync service connection configpowersync/sync-config.yaml- sync rules used by the apppowersync/cli.yaml- PowerSync CLI project link metadata
You need to:
- Enable logical replication in Neon
- Run the SQL in
db/replication.sql - Configure the PowerSync service connection in
powersync/service.yaml - Deploy the sync rules from
powersync/sync-config.yaml
The current sync rules subscribe each user to channels where they are a member and sync:
usersagentschannelschannel_membersmessagesagent_runs
Create a .env.local file with the following variables:
NEON_DATABASE_URL="postgresql://..."
POWERSYNC_SERVICE_URL=https://your-instance.powersync.com
POWERSYNC_JWT_SECRET=your-secret-min-32-chars
POWERSYNC_JWT_KID=your-key-id
OPENAI_API_KEY=sk-your-key
AI_MODEL=gpt-5Notes:
POWERSYNC_JWT_SECRETshould be a Base64URL-encoded secret.POWERSYNC_JWT_KIDis the key ID used in the JWT header.
Also add for client (Vite):
VITE_POWERSYNC_SERVICE_URL=https://your-instance.powersync.comOptional variables supported by the repo:
ANTHROPIC_API_KEY=
FIRECRAWL_API_KEY=bun devbun run testVisit http://localhost:3000
- Create a Channel: Use the form in the sidebar
- Invite an Agent: Use the agent invite UI in a channel
- Send Messages: Type in the input box
- Mention Agents: Use
@AgentNamein your message to trigger AI reply Agent execution starts after the message upload reaches the server, without blocking the client upload response. - View Agent Details: Click on agents in the right sidebar to see their description and instructions
PowerChat uses vertical slice architecture to organize features by domain rather than by technical layer. Each feature is a self-contained "slice" that includes all the code needed for that feature.
Every slice lives in src/slices/{feature-name}/ and typically contains:
index.tsx- The component/hook implementationindex.test.tsx- Test file when present (see Testing below)
Slices are categorized by their primary responsibility:
Query Slices (read-only):
- Fetch and display data
- Use
useQueryfrom~/lib/powersync-solidfor reactive local queries - Examples:
channel-list,chat-messages,username-check,channel-header,channel-member-list,channel-agents-list,agent-viewer,mention-autocomplete
Mutation Slices (write operations):
- Handle user actions that modify data
- Use PowerSync
writeTransactionor server actions - Examples:
create-channel,chat-input,username-registration,channel-invite,create-agent,delete-channel
Key Principle: Each slice is either a query OR a mutation, never both. This ensures clear separation of concerns.
Slices are completely independent - they never import or depend on other slices. This means:
- Slices can be developed, tested, and refactored in isolation
- No circular dependencies between features
- Easy to understand what each slice does without reading other code
- Route components orchestrate multiple slices together
Route components (src/routes/) compose multiple slices together:
// Example: src/routes/(chat).tsx
import { UsernameCheck } from "~/slices/username-check";
import { UsernameRegistration } from "~/slices/username-registration";
import { ChannelList } from "~/slices/channel-list";
export default function ChatLayout() {
const usernameCheck = UsernameCheck(); // Query slice
// ... conditionally render UsernameRegistration based on query state
// ... render ChannelList and other slices
}Most slices should have a colocated test file (index.test.tsx). Tests can be simple but should verify basic functionality:
- Query slices: Test that data renders correctly, loading states work, and empty states are handled
- Mutation slices: Test that user interactions trigger the correct mutations and callbacks
Tests use Vitest and @solidjs/testing-library:
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } from "@solidjs/testing-library";
import { MySlice } from "./index";
// Mock dependencies
vi.mock("~/lib/powersync-solid", () => ({
useQuery: vi.fn(() => ({ data: [], isLoading: false })),
}));
describe("MySlice", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders correctly", () => {
render(() => <MySlice prop="value" />);
expect(screen.getByText("Expected text")).toBeInTheDocument();
});
});bun test- Keep tests simple - Focus on basic functionality, not edge cases
- Mock external dependencies - Mock PowerSync queries, server actions, etc.
- Test behavior, not implementation - Verify what users see and experience
- Prefer colocated slice tests - Keep tests close to the slice and cover the primary behavior
- Messages are written to local PowerSync SQLite DB instantly
- PowerSync queues uploads to the service
- The upload handler writes validated mutations to Postgres with Drizzle
- New user messages trigger agent runs from the upload path instead of a separate DB listener
- Agent placeholder messages appear quickly, then streamed agent text is persisted in batches of roughly 20 chunks or on newline boundaries
- Replies sync back automatically