Migration from 0.25 to 0.26
To install the latest Wasp version, open your terminal and run:
npm i -g @wasp.sh/wasp-cli@latest
You can install Wasp 0.26 specifically by passing the version to the install script:
npm i -g @wasp.sh/wasp-cli@0.26
What's new in 0.26?
Your Wasp app is now one app.
Until now, a Wasp app was two programs: a client that Vite built into static files and served on port 3000, and an Express server that ran on port 3001. They found each other through REACT_APP_API_URL, talked over CORS, and were deployed as two separate artifacts.
In 0.26 they are one Nitro server that serves your pages, your prerendered routes, your API and your WebSockets. What that gets you:
- One process in development.
wasp startruns a single Vite server onhttp://localhost:3000. Your server code hot-reloads the way your client code always did, instead of restarting the whole backend. - One artifact in production.
wasp buildproduces a single self-contained server, and a single Docker image to deploy. No more building and hosting your client separately. - One origin. Your pages and your API share a URL, so
REACT_APP_API_URLand CORS are no longer part of getting your app running. - A new WebSocket API. Socket.IO is gone, replaced by Wasp's own layer over the platform's WebSockets.
Most of your app doesn't notice. Operations, CRUD, custom apis, apiNamespaces, middleware, all auth flows, jobs, emails, and prerender all work exactly as they did. The migration steps below cover the parts that do.
How to migrate?
1. Bump the Wasp version
Update the version field in your Wasp config to ^0.26.0.
- Before
- After
export default app({
wasp: { version: "^0.25.0" },
// ...
});
export default app({
wasp: { version: "^0.26.0" },
// ...
});
And run the following command to update the Wasp libraries in your project:
wasp install
2. Update your TypeScript config
Due to wasp/sdk package changes, we require some changes to your TypeScript configuration.
In tsconfig.src.json, update the include field:
- Before
- After
{
"compilerOptions": {
// ...
},
"include": ["src"]
}
{
"compilerOptions": {
// ...
},
"include": ["src", ".wasp/out/types/app"]
}
3. Rewrite your WebSocket code
Skip this step if your app doesn't use webSocket.
Wasp's WebSockets no longer run on Socket.IO. Your client code doesn't change: useSocket, useSocketListener and socket.emit all keep working, and so do the event-map interfaces you typed them with. Your server code does.
Instead of a function that receives an io server and registers connection callbacks, your webSocketFn is now a definition you create with defineWebSocket: a set of hooks Wasp calls while a connection lives.
- Before
- After
import { v4 as uuidv4 } from "uuid";
import {
type WaspSocketData,
type WebSocketDefinition,
} from "wasp/server/webSocket";
export const webSocketFn: WebSocketFn = (io, context) => {
io.on("connection", (socket) => {
const username =
socket.data.user?.getFirstProviderUserId() ?? "Unknown";
console.log("a user connected: ", username);
socket.on("chatMessage", async (msg) => {
io.emit("chatMessage", { id: uuidv4(), username, text: msg });
});
});
};
type WebSocketFn = WebSocketDefinition<
ClientToServerEvents,
ServerToClientEvents,
InterServerEvents,
SocketData
>;
interface ServerToClientEvents {
chatMessage: (msg: {
id: string;
username: string;
text: string;
}) => void;
}
interface ClientToServerEvents {
chatMessage: (msg: string) => void;
}
interface InterServerEvents {}
interface SocketData extends WaspSocketData {}
import { v4 as uuidv4 } from "uuid";
import { broadcast, defineWebSocket } from "wasp/server/webSocket";
export const webSocketFn = defineWebSocket<
ClientToServerEvents,
ServerToClientEvents
>({
open(peer) {
const username = peer.data.user?.getFirstProviderUserId() ?? "Unknown";
console.log("a user connected: ", username);
},
events: {
async chatMessage(peer, msg) {
const username =
peer.data.user?.getFirstProviderUserId() ?? "Unknown";
broadcast("chatMessage", { id: uuidv4(), username, text: msg });
},
},
});
interface ServerToClientEvents {
chatMessage: (msg: {
id: string;
username: string;
text: string;
}) => void;
}
interface ClientToServerEvents {
chatMessage: (msg: string) => void;
}
Here's how the pieces translate:
| Before (Socket.IO) | After |
|---|---|
io.on("connection", (socket) => ...) | the open(peer, context) hook |
socket.on("event", handler) | an entry in the events map |
socket.on("disconnect", handler) | the close(peer, details, context) hook |
socket.data.user | peer.data.user |
socket.emit(event, payload) | peer.send(event, payload) |
io.emit(event, payload) | broadcast(event, payload) |
socket.join(room) | peer.subscribe(topic) |
io.to(room).emit(event, payload) | publish(topic, event, payload) |
socket.broadcast.to(room).emit(...) | peer.publishToOthers(topic, event, payload) |
WebSocketDefinition<C2S, S2C, I, D> | defineWebSocket<C2S, S2C>({ ... }) |
A few things to know while you translate:
- The
InterServerEventsandSocketDatatype parameters are gone.defineWebSockettakes two: what the client sends you, and what you send the client. - Every event carries exactly one payload. An event declared as
(a: A, b: B) => voidonly carriesa. If you have multi-argument events, make them carry one object instead. TypeScript will point you at every call site. broadcastandpublishare plain functions, so you can now send events from a Query, an Action or a Job, not just from a connection's hooks.publishincludes everybody in the topic.peer.publishToOthersis the one that excludes the sender, matching Socket.IO'ssocket.broadcast.to(room).- You can refuse a connection by throwing a
Responsefrom the newupgradehook. Wasp resolves the logged-in user before it runs, sopeer.data.useris already there. socket.ioandsocket.io-clientare no longer installed. If you imported them directly (for example to use namespaces), that code needs rewriting too. See the WebSocket Channels guide for the topic-based replacement.
Read the full API in the Web Sockets docs.
4. Stop using server in your setup function
Skip this step if your app doesn't have a server setupFn, or if it only uses app.
Wasp doesn't own an HTTP server anymore, so the server your setup function receives is no longer a real one. It still type-checks, but reading anything off it throws an error explaining this, so you'll find out the first time your app starts.
The app (your Express app) is unchanged, so setup functions that add routes or middleware keep working as they are:
import { type ServerSetupFn } from "wasp/server";
export const setup: ServerSetupFn = async ({ app }) => {
app.get("/customRoute", (_req, res) => {
res.send("I am a custom route");
});
};
If you used server to attach a WebSocket server of your own, use Wasp's WebSocket support instead. If you used it for something else, come tell us on Discord so we can find you a replacement.
One more thing about setup functions: they now run once when your app's server starts, and Wasp doesn't re-run them on every code change. If you edit your setup function, restart wasp start for the change to take effect.
5. Simplify your environment variables
Your app is served from one origin now, so the variables that used to point its two halves at each other mostly go away.
REACT_APP_API_URLis no longer required. Your pages look for your API on their own origin by default, in development and in production alike. Remove it from your.env.clientand from your production build unless you deliberately serve your API from another origin.WASP_SERVER_URLis your app's public URL. It is what Wasp builds links from: the ones in the emails it sends, and the ones it redirects OAuth logins to.WASP_WEB_CLIENT_URLdefaults toWASP_SERVER_URL. Set it only when your pages really are somewhere else.- In development, both default to
http://localhost:3000. If you changed your Vite dev server's port, set both to that port in.env.server. PORTin.env.serverno longer moves your dev server. Your app's development port comes from your Vite config.PORTis still what your built app listens on (3001by default), so your deployment configuration doesn't change.
If you registered redirect URIs with an OAuth provider for local development, update them from port 3001 to port 3000:
- Before
- After
http://localhost:3001/auth/google/callback
http://localhost:3000/auth/google/callback
6. Update your deployment
wasp build now produces a single Docker image that serves your whole app.
-
Deploy one thing. Whatever you used to host your client on (a static host, a CDN bucket, a
gostaticor Caddy container) is no longer part of your deployment. Point your domain at your app, and retire the static host once your users are on it. -
Your app listens on
PORT(3001by default), and serves everything from it: pages, assets, API, and WebSockets. -
Client env vars are baked into the image. Anything
REACT_APP_*is written into your pages and assets while they are built, so it has to be there when the image is built, not when it runs. Pass it with theWASP_CLIENT_ENVbuild argument:docker build \--build-arg WASP_CLIENT_ENV="REACT_APP_EXAMPLE='value'" \-t my-wasp-app \.wasp/out -
Health checks move.
/is one of your pages now, so it no longer answers with a bare200. Point your platform's health check at/_wasp/healthinstead.
If you are using wasp deploy:
wasp deploy flyandwasp deploy railwaynow set up and deploy a single app. Runwasp deploy <provider> setupagain to have Wasp reconfigure it.- Your old client app (
<app>-clienton Fly, the-clientservice on Railway) stops receiving deployments. Wasp tells you about it when it finds one. Destroy it once your users are on your app's own URL, and delete thefly-client.tomlfile if you have one. --client-secret,--skip-clientand--custom-server-urlare ignored. Client env vars are part of the build now, and there is no separate client to deploy or to point at a server.
wasp deploy and client env varsYour provider builds your app's image, and wasp deploy has no way of passing your REACT_APP_* variables to that build yet. If your app has any, build and push the image yourself for now, with the WASP_CLIENT_ENV build argument shown above.
7. Update your custom Dockerfile
Skip this step if you don't have a Dockerfile in your project's root.
If you are using a custom Dockerfile, you'll have to add one new line to it:
- Before
- After
# ...
COPY sdk .wasp/out/sdk
COPY libs .wasp/out/libs
# ...
# ...
COPY sdk .wasp/out/sdk
COPY types .wasp/out/types
COPY libs .wasp/out/libs
# ...
Wasp's own Dockerfile stages were also renamed, since they build your whole app now and not just its server. If your Dockerfile continues from one of them, rename it:
| Before | After |
|---|---|
server-builder | builder |
server-production | production |
8. Check the smaller changes
These are unlikely to affect you, but they're worth a look:
- A custom
apino longer shadows a page. If a request reaches a customapipath and your handler doesn't answer it, it now falls through to your app's pages instead of returning a 404. Requests to/auth,/operationsand/crudpaths that match nothing still get a JSON 404. - Errors that aren't
HttpErrors come back as JSON. They used to be Express's HTML error page.HttpErrorresponses are unchanged. npm run bundleis gone, along withrollup,nodemonanddotenv, from the generated server. If your scripts called it, they should callnpx vite build(or justdocker build) instead.- Editing your
vite.config.tsneeds awasp startrestart, the same way editing your setup function does.
9. Enjoy your updated Wasp app
That's it!