Alexis Dev
HomeProjectsContact

Alexis Costa

Full Stack Developer building modern, efficient and scalable web applications.

Pilar, Buenos Aires, Argentina 🇦🇷

Repositories

  • relay-backend
  • quote-generator
  • annihilation
  • kronos

Connect

Send a messageJoin Discordalesideveloper@gmail.com

© 2026 Alexis Costa — All rights reserved.

Back
Featured

relay-frontend

Frontend for Relay, a real-time Slack-style chat app. Built with React, TypeScript, Tailwind CSS and WebSocket (STOMP) for live messaging, and installable as a PWA on iOS and Android.

0 stars0 forksTypeScriptAugust 26, 2026
ReactTypeScriptTailwind CSSWebSocket/STOMPDockerPWA

README

Relay

A Slack-style chat client built with Next.js for relay-backend, a Spring Boot microservices backend (auth, workspaces, channels, messaging, presence, files, notifications — each its own service behind an API gateway).

Relay is not a standalone app: it needs relay-backend running to do anything beyond render the login screen. See Prerequisites.

Features

  • Auth — register/login, session refresh via an httpOnly cookie, CSRF-protected
  • Workspaces & channels — create/rename, public/private channels, role-based access (OWNER/ADMIN/MEMBER on workspaces, OWNER/MEMBER on channels)
  • Messaging — real-time via WebSocket (STOMP over SockJS), threads, emoji reactions, edit/delete, consecutive-message grouping, markdown content (sanitized before render)
  • File attachments — upload and attach files/images to a message; images render inline, other files as a download chip
  • Presence & typing indicators — live online/offline status and per-channel typing indicators over a dedicated presence WebSocket
  • Notifications — paginated inbox, unread badge, mark-as-read
  • User profiles — click any avatar/name to open a profile card (avatar, banner, presence, join date); edit your own display name, avatar, and banner from Settings
  • Workspace administration — rename a workspace, manage its members, all from one settings page
  • Theming — light/dark/system, persisted, no flash on load
  • Access control enforced end-to-end — the frontend hides actions a user isn't allowed to take, but every check is also backed by the corresponding relay-backend authorization rule (see Notes on the backend contract)

Tech stack

| | | | ------------------ | --------------------------------------------------------------------------------------------------------------- | | Framework | Next.js 16 (App Router, Turbopack) + React 19 | | Language | TypeScript | | Styling | Tailwind CSS v4 | | UI primitives | Base UI + a shadcn-style component layer in src/components/ui | | Server state | TanStack Query | | Client state | Zustand | | Forms & validation | react-hook-form + Zod | | Realtime | @stomp/stompjs over SockJS | | Icons | lucide-react | | Markdown | react-markdown + remark-gfm + rehype-sanitize | | Testing | Vitest + Testing Library (unit), Playwright (e2e) | | Tooling | ESLint, Prettier, Husky + lint-staged |

Prerequisites

  1. Node.js 20+ (required by Next.js 16 / React 19) and npm.
  2. relay-backend running locally, reachable at the URL you set in NEXT_PUBLIC_API_BASE_URL. relay-backend is a separate repo — this frontend won't build a session, load a workspace, or do anything backend-dependent without it. Its api-gateway typically runs at http://localhost:8080, with GET /actuator/health as a quick liveness check.

Getting started

cp .env.example .env.local   # then fill in the values, see below
npm install
npm run dev

Open http://localhost:3000.

Environment variables

Set in .env.local (see .env.example):

| Variable | Required | Description | | -------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | NEXT_PUBLIC_API_BASE_URL | yes | URL of relay-backend's api-gateway. Public — the browser calls it directly for both REST and WebSocket traffic. Defaults to http://localhost:8080. | | CSRF_SECRET | yes | Server-only secret used to sign the CSRF double-submit token issued by the BFF auth routes (src/app/api/auth/*). Generate with openssl rand -base64 32. |

Available scripts

| Command | Description | | --------------------------------- | -------------------------------- | | npm run dev | Start the dev server (Turbopack) | | npm run build | Production build | | npm run start | Serve a production build | | npm run lint | ESLint | | npm run format / format:check | Prettier write / check | | npm run typecheck | tsc --noEmit | | npm run test / test:watch | Unit tests (Vitest) | | npm run test:e2e | End-to-end tests (Playwright) |

A Husky pre-commit hook runs lint-staged (ESLint --fix + Prettier) on staged files automatically — no manual step needed.

Project structure

src/
  app/                    Next.js App Router routes
    (auth)/                 login, register — public, redirects away if already signed in
    (app)/                  everything behind RequireAuth
      workspaces/            workspace list, per-workspace layout + channel view + settings
      notifications/
      settings/               your own profile + appearance
    api/auth/                BFF routes: login/logout/refresh/session (see below)
  features/                One folder per domain, each shaped the same way:
    <feature>/
      api.ts                  fetch calls to relay-backend, Zod-validated responses
      types.ts                 Zod schemas + inferred types, client-side validation
      hooks.ts                 TanStack Query hooks (queries + mutations)
      store.ts                 Zustand store, only where genuinely client-only state is needed
      components/
    auth/ workspaces/ channels/ messages/ presence/ files/ notifications/ settings/
  components/
    ui/                     Base UI-backed primitives (button, dialog, dropdown-menu, ...)
    layout/                 App shell pieces (topbar, empty states)
  lib/
    api/                    Shared fetch client, error type, pagination helper, config
    auth/                   JWT decode, cookie helpers, Zod session schemas, auth Zustand store
    ws/                      STOMP client factory + messaging/presence socket hooks
    avatarColor.ts           Deterministic per-id color (avatars, workspace icons)
    useObjectUrl.ts          Fetch an authenticated image endpoint into a blob URL for <img>

Architecture notes

Auth is a BFF (backend-for-frontend) pattern. The access token lives only in memory (a Zustand store) — never in localStorage or a JS-readable cookie. The refresh token is an httpOnly cookie, set by src/app/api/auth/login (a Next.js Route Handler that proxies to relay-backend and sets the cookie on the response). A page reload calls src/app/api/auth/session to silently redeem that cookie for a fresh access token. lib/api/client.ts's apiFetch attaches the Bearer token to every relay-backend call and retries once on 401 via a de-duplicated refresh (concurrent 401s share one in-flight refresh call, since the refresh token is single-use).

Authenticated image/file content. Endpoints like a user's avatar/banner or a message's file attachment require the same Bearer auth as everything else — a plain <img src="..."> can't attach a header. lib/useObjectUrl.ts fetches the endpoint via apiFetch, turns the response into a blob URL, and manages its lifecycle (revoke on unmount/change).

Realtime is two separate STOMP connections (lib/ws/): one for messaging (per-channel subscription, reconnects re-subscribe automatically) and one for presence + typing, held open for the app session. Sending a message is a plain REST POST, not a WS publish — the server broadcasts it back over the socket to everyone including the sender.

Security headers. next.config.ts sets a Content-Security-Policy (scoped to NEXT_PUBLIC_API_BASE_URL for connect-src, relaxed for inline scripts only in development), plus X-Content-Type-Options, Referrer-Policy, and X-Frame-Options: DENY.

Message content is sanitized. MessageContent renders markdown through rehype-sanitize — never dangerouslySetInnerHTML on anything that came from the API.

Notes on the backend contract

relay-backend's contract has evolved alongside this frontend (new endpoints added as features needed them — username lookup, message attachments, user avatar/banner, workspace membership authorization). A few things worth knowing if you're touching backend-dependent code:

  • Each service exposes /v3/api-docs (springdoc) — check it directly rather than assuming a shape, since it's the source of truth and has changed mid-development more than once.
  • Not everything documented in the OpenAPI spec is complete: for example SendMessageRequest.content is documented as minLength: 0 but the backend actually enforces "content, at least one attachment, or both" — verify behavior live, not just against the spec, when something looks like it should work but doesn't.
  • Workspace/channel authorization (who can add/remove members, rename, etc.) is enforced server-side, not just hidden in the UI — the frontend's role-based gating (useMyWorkspaceRole, useIsChannelMember, ...) mirrors what the backend actually checks.

Testing

Tooling is set up but no test suites have been written yet — both commands below currently exit with nothing to run.

  • Unit tests: npm run test / test:watch — Vitest + Testing Library, jsdom environment (vitest.config.ts, vitest.setup.ts). Add spec files under src/**/*.{test,spec}.ts(x).
  • E2E: npm run test:e2e — Playwright is a dependency but there's no playwright.config.ts or e2e/ directory yet; add one before this script will do anything. Would need relay-backend running, same as npm run dev.
View on GitHub← Back