SSR Frameworks
SSR Frameworks Connect an SSR or CSR app to Enfyra with same-origin REST, OAuth cookies, refresh-token support, and Socket.IO. Frameworks with SDK support — use the SDK instead of manual proxy setup If your app uses one of the frameworks below, start with the corresponding SDK pa
SSR Frameworks
Connect an SSR or CSR app to Enfyra with same-origin REST, OAuth cookies, refresh-token support, and Socket.IO.
Frameworks with SDK support — use the SDK instead of manual proxy setup
If your app uses one of the frameworks below, start with the corresponding SDK package. Nuxt and Next.js configure their framework proxy for you. Vue and React remain CSR-only and still need a same-origin proxy; follow their SDK guides before using the manual reference below. Realtime availability and setup are package-specific.
| Framework | Package | Guide |
|---|---|---|
| Nuxt 3/4 | @enfyra/sdk-nuxt |
SDK Nuxt |
| Next.js App Router | @enfyra/sdk-next |
SDK Next.js |
| Vue 3 (CSR) | @enfyra/sdk-vue |
SDK Vue |
| React (CSR) | @enfyra/sdk-react |
SDK React |
| Node.js / worker / edge | @enfyra/sdk-core |
SDK Core Client |
Only continue reading below if your framework does not have an SDK (Angular, SvelteKit, Remix) or you need to understand the proxy/cookie mechanics that the SDK abstracts.
Integration Model
Browser
-> your SSR app origin
-> local Enfyra proxy prefix
-> Enfyra app /api and /ws bridge
-> Enfyra server
Do not call the raw Enfyra server from browser code. Browser code should call your app origin only.
Required Proxies
Expose two paths from your app:
| Local path | Forward to | Purpose |
|---|---|---|
/enfyra/** |
https://demo.enfyra.io/api/** |
REST, auth, OAuth, files, GraphQL |
/socket.io/** |
https://demo.enfyra.io/ws/socket.io/** |
Socket.IO transport |
You may choose another REST prefix, but keep it stable. If your prefix is /enfyra, OAuth must pass cookieBridgePrefix=/enfyra.
Shared Browser Code
After the proxy exists, browser calls are the same in every SSR framework.
Password login:
await fetch("/enfyra/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ email, password, remember: true }),
})
Current user:
const me = await fetch("/enfyra/me", {
credentials: "include",
})
OAuth starts on the Enfyra app URL, then returns through your app proxy:
const redirect = new URL("/chat", window.location.origin)
const url = new URL("/api/auth/google", "https://demo.enfyra.io")
url.searchParams.set("redirect", redirect.toString())
url.searchParams.set("cookieBridgePrefix", "/enfyra")
window.location.href = url.toString()
Socket.IO:
import { io } from "socket.io-client"
const socket = io("/chat", {
path: "/socket.io",
withCredentials: true,
reconnection: false,
transports: ["polling"],
upgrade: false,
})
/chat is the Enfyra websocket namespace. /socket.io is the transport path your SSR app proxies to the Enfyra app websocket bridge.
The demo chat apps use polling-only Socket.IO because it works consistently through simple SSR framework rewrites and reverse proxies. If your deployment proxy supports websocket upgrade cleanly, you can enable websocket transport later.
OAuth Flow
User clicks Google
-> https://demo.enfyra.io/api/auth/google?redirect=...&cookieBridgePrefix=/enfyra
-> Enfyra redirects to Google
-> Google returns to Enfyra callback
-> Enfyra redirects through {yourAppOrigin}/enfyra/auth/set-cookies
-> the proxy response sets cookies on your app origin
-> browser returns to redirect
The user does not handle tokens manually.
Nuxt
SDK available: If you are starting a new Nuxt project with Enfyra, use
@enfyra/sdk-nuxtinstead of manual proxy configuration. The section below is for understanding or customizing the proxy manually.
Use routeRules.
export default defineNuxtConfig({
routeRules: {
"/enfyra/**": {
proxy: {
to: "https://demo.enfyra.io/api/**",
fetchOptions: { redirect: "manual" },
},
},
"/socket.io/**": {
proxy: {
to: "https://demo.enfyra.io/ws/socket.io/**",
},
},
},
})
redirect: "manual" keeps OAuth and set-cookie redirects under your app origin.
Next.js
SDK available: If you are starting a new Next.js project with Enfyra, use
@enfyra/sdk-nextinstead of manual rewrites. The section below is for understanding or customizing the proxy manually.
Use rewrites().
const nextConfig = {
async rewrites() {
return [
{
source: "/enfyra/:path*",
destination: "https://demo.enfyra.io/api/:path*",
},
{
source: "/socket.io/",
destination: "https://demo.enfyra.io/ws/socket.io/",
},
]
},
}
export default nextConfig
For server components, forward the incoming cookie header when fetching through your own app URL:
import { cookies } from "next/headers"
const res = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/enfyra/me`, {
headers: { cookie: (await cookies()).toString() },
cache: "no-store",
})
Angular
No SDK yet. Angular does not have an Enfyra SDK package. Use the manual proxy configuration below.
Use the Angular dev-server proxy for local development and configure the same routes in your production reverse proxy.
{
"/enfyra/**": {
"target": "https://demo.enfyra.io/api",
"secure": true,
"changeOrigin": true,
"pathRewrite": {
"^/enfyra": ""
}
},
"/socket.io/**": {
"target": "https://demo.enfyra.io/ws",
"secure": true,
"changeOrigin": true,
"ws": true
}
}
Add the proxy file to the Angular serve target:
{
"projects": {
"your-app": {
"architect": {
"serve": {
"options": {
"proxyConfig": "src/proxy.conf.json"
}
}
}
}
}
}
Use an HTTP interceptor so every Enfyra request sends cookies:
import { ApplicationConfig, inject } from "@angular/core"
import { HttpInterceptorFn, provideHttpClient, withInterceptors } from "@angular/common/http"
import { CanActivateFn, provideRouter, Router } from "@angular/router"
import { catchError, map, of } from "rxjs"
import { routes } from "./app.routes"
import { EnfyraAuthService } from "./enfyra-auth.service"
export const enfyraCredentialsInterceptor: HttpInterceptorFn = (req, next) => {
if (!req.url.startsWith("/enfyra/")) return next(req)
return next(req.clone({ withCredentials: true }))
}
export const requireUserGuard: CanActivateFn = () => {
const auth = inject(EnfyraAuthService)
const router = inject(Router)
return auth.loadMe().pipe(
map(user => user ? true : router.createUrlTree(["/login"])),
catchError(() => of(router.createUrlTree(["/login"]))),
)
}
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withInterceptors([enfyraCredentialsInterceptor])),
provideRouter(routes),
],
}
Create a small auth service around /enfyra/login, /enfyra/me, and /enfyra/logout:
import { Injectable, signal } from "@angular/core"
import { HttpClient } from "@angular/common/http"
import { Observable, switchMap, tap } from "rxjs"
type EnfyraUser = {
id: string | number
email?: string
}
@Injectable({ providedIn: "root" })
export class EnfyraAuthService {
readonly user = signal<EnfyraUser | null>(null)
constructor(private readonly http: HttpClient) {}
login(email: string, password: string): Observable<EnfyraUser | null> {
return this.http.post("/enfyra/login", { email, password, remember: true }).pipe(
switchMap(() => this.loadMe()),
)
}
loadMe(): Observable<EnfyraUser | null> {
return this.http.get<EnfyraUser | null>("/enfyra/me").pipe(
tap(user => this.user.set(user)),
)
}
logout(): Observable<unknown> {
return this.http.post("/enfyra/logout", {}).pipe(
tap(() => this.user.set(null)),
)
}
startGoogleOAuth(returnPath = "/") {
const redirect = new URL(returnPath, window.location.origin)
const url = new URL("/api/auth/google", "https://demo.enfyra.io")
url.searchParams.set("redirect", redirect.toString())
url.searchParams.set("cookieBridgePrefix", "/enfyra")
window.location.href = url.toString()
}
}
Create one Socket.IO connection in an app-level service after auth is known:
import { computed, effect, Injectable, signal } from "@angular/core"
import { io, type Socket } from "socket.io-client"
import { EnfyraAuthService } from "./enfyra-auth.service"
@Injectable({ providedIn: "root" })
export class EnfyraRealtimeService {
private socket: Socket | null = null
private readonly connected = signal(false)
readonly isConnected = computed(() => this.connected())
constructor(private readonly auth: EnfyraAuthService) {
effect(() => {
const user = this.auth.user()
if (user) this.connect()
else this.disconnect()
})
}
connect() {
if (this.socket) return this.socket
this.socket = io("/chat", {
path: "/socket.io",
withCredentials: true,
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 2000,
reconnectionDelayMax: 30000,
transports: ["polling"],
upgrade: false,
})
this.socket.on("connect", () => this.connected.set(true))
this.socket.on("disconnect", () => this.connected.set(false))
return this.socket
}
disconnect() {
this.socket?.disconnect()
this.socket = null
this.connected.set(false)
}
onMessage(handler: (event: unknown) => void) {
const activeSocket = this.connect()
activeSocket.on("chat:message", handler)
return () => activeSocket.off("chat:message", handler)
}
}
Route guards are for browser navigation only. Keep Enfyra route permissions, guards, and owner checks on the server side.
SvelteKit
No SDK yet. SvelteKit does not have an Enfyra SDK package. Use the manual server hook proxy below.
Use a server hook for the REST/auth prefix.
import type { Handle } from "@sveltejs/kit"
const ENFYRA_API = "https://demo.enfyra.io/api"
export const handle: Handle = async ({ event, resolve }) => {
if (!event.url.pathname.startsWith("/enfyra/")) {
return resolve(event)
}
const upstream = new URL(
event.url.pathname.replace(/^\/enfyra/, ""),
ENFYRA_API,
)
upstream.search = event.url.search
return fetch(upstream, {
method: event.request.method,
headers: event.request.headers,
body:
event.request.method === "GET" || event.request.method === "HEAD"
? undefined
: event.request.body,
redirect: "manual",
})
}
Configure your deployment proxy to forward /socket.io/** to https://demo.enfyra.io/ws/socket.io/**.
Remix
No SDK yet. Remix does not have an Enfyra SDK package. Use the manual catch-all resource route below.
Create a catch-all resource route for /enfyra/*.
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"
const ENFYRA_API = "https://demo.enfyra.io/api"
async function proxy(request: Request, params: { "*": string }) {
const url = new URL(request.url)
const upstream = new URL(`/${params["*"] ?? ""}`, ENFYRA_API)
upstream.search = url.search
return fetch(upstream, {
method: request.method,
headers: request.headers,
body:
request.method === "GET" || request.method === "HEAD"
? undefined
: request.body,
redirect: "manual",
})
}
export const loader = ({ request, params }: LoaderFunctionArgs) =>
proxy(request, params)
export const action = ({ request, params }: ActionFunctionArgs) =>
proxy(request, params)
Configure your hosting proxy to forward /socket.io/** to https://demo.enfyra.io/ws/socket.io/**.
Common Mistakes
Missing cookieBridgePrefix
If the app proxy prefix is /enfyra, OAuth must include:
cookieBridgePrefix=/enfyra
Without this, Enfyra uses the default /api cookie bridge, which is correct for the Enfyra app itself but wrong for a third-party app using /enfyra.
Starting OAuth from the wrong origin
The production demo chat apps start OAuth on the Enfyra app URL:
new URL("/api/auth/google", "https://demo.enfyra.io")
They do not start Google OAuth at /enfyra/auth/google. The local /enfyra/** proxy is still required for the later /enfyra/auth/set-cookies bridge.
Calling the backend host from browser code
Avoid:
fetch("https://raw-server.example.com/project_task")
Use:
fetch("/enfyra/project_task", { credentials: "include" })
Connecting to /ws/chat
Do not treat Enfyra realtime as a raw WebSocket path. Use Socket.IO namespace plus transport path:
io("/chat", { path: "/socket.io", withCredentials: true })
Next Step
After your framework is integrated, build a real feature with Third-Party Chat App Example.