Skip to content

Fix: Wasp Not Working — Build Failing, Auth Not Working, or Operations Returning Empty

FixDevs ·

Quick Answer

How to fix Wasp full-stack framework issues — main.wasp configuration, queries and actions, authentication setup, Prisma integration, job scheduling, and deployment.

The Problem

wasp start fails with a configuration error:

wasp start
# Error: Invalid Wasp configuration — or —
# Error: Prisma schema validation failed

Or queries return empty results:

const { data: tasks } = useQuery(getTasks);
// data is undefined or empty array

Or authentication doesn’t redirect after login:

Login succeeds but user stays on the login page

Why This Happens

Wasp is a full-stack framework that uses a declarative DSL (.wasp file) alongside React and Node.js code:

  • main.wasp is the source of truth — it declares routes, operations (queries/actions), auth, jobs, and entities. The framework generates boilerplate code from this file. Syntax errors in the .wasp file stop everything.
  • Entities are Prisma models — Wasp uses Prisma under the hood. Entity definitions in the .wasp file map to Prisma models. Database operations use the Prisma client.
  • Operations have server-side implementations — queries and actions declared in .wasp point to TypeScript functions in src/server/. If the import path is wrong or the function doesn’t exist, operations silently fail.
  • Auth is declarative — authentication providers (email/password, Google, GitHub) are configured in .wasp. The actual auth flow is generated — you don’t write login logic manually.

Fix 1: Basic Wasp Configuration

curl -sSL https://get.wasp-lang.dev/installer.sh | sh
wasp new my-app
cd my-app
wasp start
// main.wasp — declarative configuration
app MyApp {
  wasp: { version: "^0.14.0" },
  title: "My App",
  
  head: [
    "<meta name='description' content='My Wasp App' />"
  ],

  auth: {
    userEntity: User,
    methods: {
      usernameAndPassword: {},
      google: {},
      github: {},
    },
    onAuthFailedRedirectTo: "/login",
    onAuthSucceededRedirectTo: "/dashboard",
  },

  db: {
    system: PostgreSQL,
    // seeds: [import { seedDevData } from "@src/server/seeds"]
  },
}

// Entities (Prisma models)
entity User {=psl
  id       Int      @id @default(autoincrement())
  tasks    Task[]
  createdAt DateTime @default(now())
psl=}

entity Task {=psl
  id          Int      @id @default(autoincrement())
  description String
  isDone      Boolean  @default(false)
  user        User     @relation(fields: [userId], references: [id])
  userId      Int
  createdAt   DateTime @default(now())
psl=}

// Routes
route RootRoute { path: "/", to: MainPage }
route LoginRoute { path: "/login", to: LoginPage }
route SignupRoute { path: "/signup", to: SignupPage }
route DashboardRoute { path: "/dashboard", to: DashboardPage }

// Pages
page MainPage {
  component: import { MainPage } from "@src/client/MainPage"
}
page LoginPage {
  component: import { LoginPage } from "@src/client/LoginPage"
}
page SignupPage {
  component: import { SignupPage } from "@src/client/SignupPage"
}
page DashboardPage {
  authRequired: true,
  component: import { DashboardPage } from "@src/client/DashboardPage"
}

// Queries (read operations)
query getTasks {
  fn: import { getTasks } from "@src/server/queries",
  entities: [Task]
}

query getTask {
  fn: import { getTask } from "@src/server/queries",
  entities: [Task]
}

// Actions (write operations)
action createTask {
  fn: import { createTask } from "@src/server/actions",
  entities: [Task]
}

action updateTask {
  fn: import { updateTask } from "@src/server/actions",
  entities: [Task]
}

action deleteTask {
  fn: import { deleteTask } from "@src/server/actions",
  entities: [Task]
}

Fix 2: Server Operations

// src/server/queries.ts
import { type GetTasks, type GetTask } from 'wasp/server/operations';

export const getTasks: GetTasks<void, Task[]> = async (args, context) => {
  if (!context.user) throw new HttpError(401, 'Not authenticated');

  return context.entities.Task.findMany({
    where: { userId: context.user.id },
    orderBy: { createdAt: 'desc' },
  });
};

export const getTask: GetTask<{ id: number }, Task> = async ({ id }, context) => {
  if (!context.user) throw new HttpError(401);

  const task = await context.entities.Task.findUnique({ where: { id } });
  if (!task || task.userId !== context.user.id) {
    throw new HttpError(404, 'Task not found');
  }

  return task;
};
// src/server/actions.ts
import { type CreateTask, type UpdateTask, type DeleteTask } from 'wasp/server/operations';
import { HttpError } from 'wasp/server';

export const createTask: CreateTask<{ description: string }, Task> = async ({ description }, context) => {
  if (!context.user) throw new HttpError(401);

  return context.entities.Task.create({
    data: {
      description,
      userId: context.user.id,
    },
  });
};

export const updateTask: UpdateTask<{ id: number; isDone: boolean }, Task> = async ({ id, isDone }, context) => {
  if (!context.user) throw new HttpError(401);

  const task = await context.entities.Task.findUnique({ where: { id } });
  if (!task || task.userId !== context.user.id) {
    throw new HttpError(404);
  }

  return context.entities.Task.update({
    where: { id },
    data: { isDone },
  });
};

export const deleteTask: DeleteTask<{ id: number }, void> = async ({ id }, context) => {
  if (!context.user) throw new HttpError(401);

  await context.entities.Task.delete({ where: { id } });
};

Fix 3: Client Pages

// src/client/DashboardPage.tsx
import { useQuery, useAction } from 'wasp/client/operations';
import { getTasks, createTask, updateTask, deleteTask } from 'wasp/client/operations';
import { useState } from 'react';

export function DashboardPage() {
  const { data: tasks, isLoading, error } = useQuery(getTasks);
  const createTaskFn = useAction(createTask);
  const updateTaskFn = useAction(updateTask);
  const deleteTaskFn = useAction(deleteTask);
  const [newTask, setNewTask] = useState('');

  async function handleCreate(e: React.FormEvent) {
    e.preventDefault();
    if (!newTask.trim()) return;

    await createTaskFn({ description: newTask });
    setNewTask('');
    // Wasp automatically invalidates the getTasks query
  }

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <h1>Dashboard</h1>

      <form onSubmit={handleCreate}>
        <input
          value={newTask}
          onChange={(e) => setNewTask(e.target.value)}
          placeholder="New task..."
        />
        <button type="submit">Add</button>
      </form>

      <ul>
        {tasks?.map(task => (
          <li key={task.id}>
            <input
              type="checkbox"
              checked={task.isDone}
              onChange={() => updateTaskFn({ id: task.id, isDone: !task.isDone })}
            />
            <span style={{ textDecoration: task.isDone ? 'line-through' : 'none' }}>
              {task.description}
            </span>
            <button onClick={() => deleteTaskFn({ id: task.id })}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Fix 4: Authentication Pages

// src/client/LoginPage.tsx
import { LoginForm } from 'wasp/client/auth';
import { Link } from 'wasp/client/router';

export function LoginPage() {
  return (
    <div style={{ maxWidth: 400, margin: '0 auto', padding: 20 }}>
      <h1>Login</h1>
      <LoginForm />
      <p>
        Don't have an account? <Link to="/signup">Sign up</Link>
      </p>
    </div>
  );
}

// src/client/SignupPage.tsx
import { SignupForm } from 'wasp/client/auth';
import { Link } from 'wasp/client/router';

export function SignupPage() {
  return (
    <div style={{ maxWidth: 400, margin: '0 auto', padding: 20 }}>
      <h1>Sign Up</h1>
      <SignupForm />
      <p>
        Already have an account? <Link to="/login">Log in</Link>
      </p>
    </div>
  );
}

// Access user in any component
import { useAuth } from 'wasp/client/auth';

function UserMenu() {
  const { data: user } = useAuth();

  if (!user) return <Link to="/login">Log in</Link>;

  return (
    <div>
      <span>Welcome, {user.identities.username?.id || user.id}</span>
      <button onClick={() => window.location.href = '/auth/logout'}>Logout</button>
    </div>
  );
}

Fix 5: Background Jobs

// main.wasp — declare a job
job dailyReport {
  executor: PgBoss,
  perform: {
    fn: import { generateDailyReport } from "@src/server/jobs"
  },
  schedule: {
    cron: "0 9 * * *"  // Every day at 9 AM
  },
  entities: [Task, User]
}

job sendEmail {
  executor: PgBoss,
  perform: {
    fn: import { sendEmail } from "@src/server/jobs"
  },
  entities: [User]
}
// src/server/jobs.ts
import { type DailyReport, type SendEmail } from 'wasp/server/jobs';

export const generateDailyReport: DailyReport<void, void> = async (args, context) => {
  const users = await context.entities.User.findMany({
    include: { tasks: true },
  });

  for (const user of users) {
    const completedToday = user.tasks.filter(t =>
      t.isDone && isToday(t.createdAt)
    ).length;

    await sendReportEmail(user, completedToday);
  }
};

export const sendEmail: SendEmail<{ to: string; subject: string; html: string }, void> = async ({ to, subject, html }) => {
  await emailProvider.send({ to, subject, html });
};

// Trigger a job manually from an action
import { sendEmail } from 'wasp/server/jobs';

export const inviteUser = async ({ email }, context) => {
  await sendEmail.submit({
    to: email,
    subject: 'You are invited!',
    html: '<p>Welcome!</p>',
  });
};

Fix 6: Deployment

# Deploy to Fly.io
wasp deploy fly setup
wasp deploy fly cmd secrets set DATABASE_URL=postgres://...
wasp deploy fly deploy

# Or build for self-hosting
wasp build
# Output in .wasp/build/
# Contains Dockerfile and docker-compose for both client and server

Still Not Working?

Wasp configuration error — check main.wasp syntax. Common issues: missing commas in entity definitions, wrong import paths (must use @src/ prefix), or referencing entities not declared in the operation’s entities list.

Queries return empty — the operation needs entities: [Task] in the .wasp declaration to access context.entities.Task. Without it, the Prisma client for that entity isn’t available. Also check auth — context.user is null for unauthenticated requests.

Auth redirect looponAuthSucceededRedirectTo in the auth config must point to a route that exists and doesn’t require auth itself. If the redirect target also requires auth, you get a loop.

Database errors after changing entities — run wasp db migrate-dev after modifying entity definitions. Wasp uses Prisma migrations. Without migrating, the database schema doesn’t match the code.

For related full-stack issues, see Fix: TanStack Start Not Working and Fix: Next.js App Router Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles