···2121 // Create a store connected to multiplayer.
2222 const store = useSync({
2323 // We need to know the websocket's URI...
2424- uri: `${WORKER_URL}/connect/${roomId}`,
2424+ uri: `${WORKER_URL}/api/connect/${roomId}`,
2525 // ...and how to handle static assets like images & videos
2626 assets: multiplayerAssets,
2727 })
···8383 const id = uniqueId()
84848585 const objectName = `${id}-${file.name}`
8686- const url = `${WORKER_URL}/uploads/${encodeURIComponent(objectName)}`
8686+ const url = `${WORKER_URL}/api/uploads/${encodeURIComponent(objectName)}`
87878888 const response = await fetch(url, {
8989 method: 'PUT',
···120120 }
121121122122 try {
123123- const response = await fetch(`${WORKER_URL}/unfurl?url=${encodeURIComponent(url)}`)
123123+ const response = await fetch(`${WORKER_URL}/api/unfurl?url=${encodeURIComponent(url)}`)
124124 const data = await response.json()
125125126126 asset.props.description = data?.description ?? ''
+5-1
src/server/assets.ts
···33import { Readable } from 'stream'
4455// We are just using the filesystem to store assets
66-const DIR = resolve('./.assets')
66+let DIR = resolve('./.assets')
77+if(process.env.CONFIG_DIR !== undefined) {
88+ DIR = join(process.env.CONFIG_DIR, 'assets');
99+}
1010+console.log(`Assets: ${DIR}`);
711812export async function storeAsset(id: string, stream: Readable) {
913 await mkdir(DIR, { recursive: true })
+5-1
src/server/rooms.ts
···33import { join } from 'path'
4455// For this example we're just saving data to the local filesystem
66-const DIR = './.rooms'
66+let DIR = './.rooms'
77+if(process.env.CONFIG_DIR !== undefined) {
88+ DIR = join(process.env.CONFIG_DIR, 'rooms');
99+}
1010+console.log(`Rooms: ${DIR}`);
711async function readSnapshotIfExists(roomId: string) {
812 try {
913 const data = await readFile(join(DIR, roomId))
+29-10
src/server/server.node.ts
···11import cors from '@fastify/cors'
22import websocketPlugin from '@fastify/websocket'
33import fastify from 'fastify'
44+import path from 'node:path';
55+import ServeStatic from '@fastify/static';
46import type { RawData } from 'ws'
55-import { loadAsset, storeAsset } from './assets'
66-import { makeOrLoadRoom } from './rooms'
77-import { unfurl } from './unfurl'
77+import { loadAsset, storeAsset } from './assets.js'
88+import { makeOrLoadRoom } from './rooms.js'
99+import { unfurl } from './unfurl.js'
810911const PORT = 5858
1012···1214// To keep things simple we're skipping normal production concerns like rate limiting and input validation.
1315const app = fastify()
1416app.register(websocketPlugin)
1515-app.register(cors, { origin: '*' })
1717+app.register(cors, { origin: '*' });
1818+1919+if(process.env.NODE_ENV === 'production') {
2020+ app.register(ServeStatic, {
2121+ root: path.resolve(import.meta.dirname, '..', '..', 'client', 'assets'),
2222+ prefix: '/assets/'
2323+ });
2424+}
2525+16261727app.register(async (app) => {
1828 // This is the main entrypoint for the multiplayer sync
1919- app.get('/connect/:roomId', { websocket: true }, async (socket, req) => {
2929+ app.get('/api/connect/:roomId', { websocket: true }, async (socket, req) => {
2030 // The roomId comes from the URL pathname
2131 const roomId = (req.params as any).roomId as string
2232 // The sessionId is passed from the client as a query param,
···5262 // To enable blob storage for assets, we add a simple endpoint supporting PUT and GET requests
5363 // But first we need to allow all content types with no parsing, so we can handle raw data
5464 app.addContentTypeParser('*', (_, __, done) => done(null))
5555- app.put('/uploads/:id', {}, async (req, res) => {
6565+ app.put('/api/uploads/:id', {}, async (req, res) => {
5666 const id = (req.params as any).id as string
5767 await storeAsset(id, req.raw)
5868 res.send({ ok: true })
5969 })
6060- app.get('/uploads/:id', async (req, res) => {
7070+ app.get('/api/uploads/:id', async (req, res) => {
6171 const id = (req.params as any).id as string
6272 const data = await loadAsset(id)
6373 res.send(data)
6474 })
65756676 // To enable unfurling of bookmarks, we add a simple endpoint that takes a URL query param
6767- app.get('/unfurl', async (req, res) => {
7777+ app.get('/api/unfurl', async (req, res) => {
6878 const url = (req.query as any).url as string
6979 res.send(await unfurl(url))
7070- })
7171-})
8080+ });
8181+8282+ if(process.env.NODE_ENV === 'production') {
8383+ app.setNotFoundHandler((req, res) => {
8484+ res.sendFile('index.html', path.resolve(import.meta.dirname, '..', '..', 'client'))
8585+ });
8686+ // app.get('/path/with/different/root', function (req, reply) {
8787+ // reply.sendFile('index.html', path.resolve(import.meta.dirname, '..', '..', 'client')) // serving a file from a different root location
8888+ // });
8989+ }
9090+});
72917392app.listen({ port: PORT, host: '0.0.0.0' }, (err) => {
7493 if (err) {