Skip to content
nustechnologyPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Folio — Grounded Research Workspace

License Node.js Next.js

Folio is a research workspace by NUS Technology for turning personal source material into grounded answers, traceable notes, and a working notebook.

Users organize research into spaces, add files, web pages, or manual text as sources, and ask questions against either an entire space or one selected source. Answers stream into the interface with citations that link back to the exact evidence passage. Useful answers can be saved as notes, and notes can be converted into reusable sources.

This repository contains the Folio web application. It is a Next.js App Router frontend that talks to the Folio backend through a same-origin /api/* route in development and production.

Table of contents


Tech stack

Layer Technology
Framework Next.js 16.2.12 with the App Router
UI runtime React 19.2.4, React DOM 19.2.4
Language TypeScript 5 in strict mode
Styling Tailwind CSS 4, CSS design tokens, tw-animate-css
UI primitives Radix UI, reusable shadcn-style components, Lucide icons
Server state TanStack Query 5
Forms and validation React Hook Form 7, Zod 4, @hookform/resolvers
Rich text Tiptap 3, Marked, isomorphic DOMPurify
Streaming Native SSE reader for answers, @microsoft/fetch-event-source for source-processing status
Authentication Bearer access and refresh tokens stored in browser cookies
Feedback Sonner toast notifications
Code quality ESLint 9, Next.js Core Web Vitals rules, Prettier 3, unused-import checks
Package manager Yarn 4.12.0 with the node-modules linker
Runtime packaging Multi-stage Docker build with Next.js standalone output

Getting started

Prerequisites

The reference development environment uses:

Tool Version Installation
Node.js 22.17.1 nvm install 22.17.1
Yarn 4.12.0 corepack enable && corepack prepare yarn@4.12.0 --activate

The repository does not include an .nvmrc or pin packageManager in package.json. Keep local development on Node.js 22 and Yarn 4 to match the Docker build. Yarn is configured through .yarnrc.yml to install a conventional node_modules directory instead of using Plug'n'Play.

Install and run

git clone git@github.com:nustechnology/Folio-Web.git
cd Folio-Web

nvm use 22.17.1
corepack enable
corepack prepare yarn@4.12.0 --activate
yarn install
yarn dev

Open http://localhost:3000.

The frontend expects the Folio backend at http://localhost:3001 by default. next.config.ts proxies browser requests from /api/* to that backend, so local frontend requests remain same-origin.

Production build

yarn build
yarn start

The build uses output: 'standalone', which emits the minimal server bundle consumed by the production Docker image.


Environment configuration

No environment file is required for normal local development. Copy .env.example to .env.local only when the defaults do not match your setup.

Variable Default Purpose
API_PROXY_TARGET http://localhost:3001 Backend origin used by the Next.js /api/* rewrite
ALLOWED_DEV_ORIGINS — Additional comma-separated hosts allowed to load Next.js development assets
NEXT_PUBLIC_API_URL /api/v1 Overrides the browser API base when the backend is not available behind /api/*
COMPOSE_FILE — VPS only: .env holds COMPOSE_FILE=docker-compose.prod.yml so bare Compose commands select the production stack

Keep NEXT_PUBLIC_API_URL unset when the frontend and backend share an origin. Because NEXT_PUBLIC_* values are embedded during the build, changing this value in production requires rebuilding the image.

Do not commit .env, .env.local, tokens, or other credentials.

Sharing development through a tunnel

Expose the frontend port only:

ngrok http 3000

The frontend continues to reach the backend through the /api/* rewrite. ngrok and Cloudflare tunnel hostnames are already included in allowedDevOrigins; use ALLOWED_DEV_ORIGINS for other hosts such as a LAN address.

A same-origin write still carries the public tunnel URL in its Origin header. The backend CORS allowlist must therefore accept that origin as well.


Project structure

folio-web/
├── app/                              # App Router pages and route layouts
│   ├── layout.tsx                    # Root providers, fonts, toast viewport
│   ├── page.tsx                      # Public landing page
│   ├── login/                        # Login server page + client form
│   ├── sign-up/                      # Sign-up server page + client form
│   └── spaces/
│       ├── page.tsx                  # Space list
│       ├── layout.tsx                # Source-processing SSE provider
│       └── [id]/
│           ├── sources/              # Source list and evidence reader
│           ├── ask/                  # Conversation history and streamed Q&A
│           ├── notes/                # Notes and answer-to-note workflow
│           └── notebook/             # Auto-saving research notebook
├── components/
│   ├── Ask/                          # Composer, messages, citations, history
│   ├── Auth/                         # Auth forms and password controls
│   ├── Note/                         # Note list, editor, viewer, conversion
│   ├── Notebook/                     # Editor, toolbar, save state, export
│   ├── Source/                       # Source CRUD, progress, reader blocks
│   ├── Space/                        # Space CRUD, grid, shell, sorting
│   └── shared/                       # App chrome, search, pagination, UI primitives
├── features/                         # Domain data and state modules
│   ├── Ask/
│   ├── Auth/
│   ├── Note/
│   ├── Notebook/
│   ├── Passage/
│   ├── Source/
│   └── Space/
├── lib/
│   ├── api/                          # Typed fetch client, tokens, refresh, base URL
│   ├── query/                        # QueryClient factory and provider
│   └── utils.ts                      # Shared class and error helpers
├── hooks/                            # Cross-domain client hooks
├── utils/                            # Pure auth, date, source, and Markdown helpers
├── public/                           # Static assets
├── next.config.ts                    # Rewrite, dev origins, standalone output
├── Dockerfile                        # Production multi-stage image
├── docker-compose.prod.yml           # Frontend-only VPS Compose service
├── eslint.config.mjs                 # ESLint 9 flat configuration
├── postcss.config.mjs                # Tailwind CSS 4 PostCSS plugin
└── package.json                      # Dependencies and Yarn scripts

Each feature follows the same data-layer shape:

features/<Domain>/
├── types.ts              # Domain and API contract types
├── <domain>.service.ts   # Pure typed API calls
├── <domain>.mutation.ts  # TanStack Query queries and mutations
├── use<Domain>.ts        # Consumer-facing hooks
└── index.ts              # Public barrel exports

Ask contains separate service and query modules for streamed answers and conversation history. Source additionally owns the global source-processing status provider, while Notebook owns its debounced auto-save hook.


System architecture

flowchart LR
  User["Researcher"]

  subgraph Browser["Browser"]
    Pages["Next.js App Router<br/>server pages"]
    UI["React client components<br/>forms and editors"]
    Providers["AuthProvider · QueryProvider<br/>SourceStatusProvider"]
    Features["Consumer hooks<br/>TanStack Query<br/>service functions"]
    Client["Typed API client<br/>auth · refresh · timeout · SSE"]
  end

  subgraph FrontendRuntime["Next.js frontend :3000"]
    Rewrite["/api/* rewrite<br/>development only in practice"]
  end

  Proxy["Nginx<br/>production /api routing"]
  Backend["Folio backend<br/>default :3001"]

  User --> Pages
  Pages --> UI
  UI --> Providers
  Providers --> Features
  Features --> Client
  Client -->|"Development /api/v1"| Rewrite
  Rewrite --> Backend
  Client -.->|"Production same-origin /api/v1"| Proxy
  Proxy --> Backend
  Backend -->|"Answer token stream"| Client
  Backend -->|"Source status events"| Providers
Loading

The main boundaries are intentional:

  • Pages remain Server Components and pass route data into focused Client Components when interactivity is required.
  • UI components consume domain hooks rather than calling the API client directly.
  • Service modules know endpoint paths and payload types but contain no React or cache logic.
  • TanStack Query modules own caching, query keys, invalidation, and mutations.
  • The shared API client owns bearer headers, token refresh, request cancellation, timeouts, response parsing, and normalized errors.
  • The backend remains a separate service. In development Next.js forwards /api/*; in production Nginx routes the same path before Next.js sees it.

Data flow

flowchart LR
  Component["Domain component"] --> Hook["Consumer hook"]
  Hook --> Query["Query / mutation hook"]
  Query --> Service["Service function"]
  Service --> Api["Shared API client"]
  Api --> Backend["Folio backend"]
  Backend --> Api
  Api --> Service
  Service --> Query
  Query -->|"cache update / invalidation"| Component
Loading

Core product flows

Source ingestion and evidence navigation

  1. A user adds a file, URL, or manual text to a research space.
  2. The source enters added, extracting_text, indexing_evidence, and finally ready or failed.
  3. SourceStatusProvider keeps one authenticated SSE connection open at /sources/status and exposes per-source progress to the UI.
  4. A transition to ready or failed invalidates the source list, source detail, and space queries.
  5. Ready sources expose structured content and evidence passages in the source reader.
  6. Citation links use #evidence-passage-<id> anchors to scroll to and highlight the exact supporting passage.

Grounded question answering

sequenceDiagram
  autonumber
  participant User
  participant UI as Ask UI
  participant API as Shared API client
  participant Backend as Folio backend

  User->>UI: Submit a question
  UI->>API: POST /spaces/:id/ask
  API->>Backend: JSON payload + bearer token
  Backend-->>API: start event
  API-->>UI: Conversation and message IDs
  Backend-->>API: token events
  API-->>UI: Append answer text progressively
  Backend-->>API: citations event
  API-->>UI: Attach evidence links
  Backend-->>API: done event
  API-->>UI: Replace draft with canonical answer
Loading

Questions may target the whole space or one source. The closing done frame is authoritative because the backend may normalize citation markers after selecting the final evidence set. Users can rate an answer, open its citations, or save it to Notes with the citation mapping intact.

Notes and notebook

  • Notes may be written manually or created from an assistant answer.
  • Rich-text note content is rendered through Tiptap and sanitized before display.
  • A note can be converted into a source; the backend snapshots the stored note content.
  • Each space has one working notebook with debounced auto-save.
  • Notebook saves are serialized to avoid overlapping requests, flushed on navigation or tab hide, and blocked when the document exceeds the backend limit.
  • Notebooks can be exported as Markdown or through the browser print dialog for PDF output.

Authentication and API client

Folio uses access and refresh tokens returned by the backend:

  1. Login returns tokens; sign-up returns tokens plus the newly created user.
  2. The browser stores both tokens in same-site cookies. Secure cookies are enabled in production.
  3. Authenticated requests attach Authorization: Bearer <access token>.
  4. AuthProvider loads /users/me after an access token appears.
  5. On a 401 with the backend error code TOKEN_EXPIRED, the API client deduplicates concurrent refresh attempts, stores the rotated tokens, and retries the original request once.
  6. Refresh failure or another unauthorized response clears tokens and all user-scoped query data, then redirects to /login.
  7. Explicit logout calls the backend first and always performs local teardown when the request settles.

The API client also provides:

  • JSON, multipart, GET, POST, PATCH, PUT, and DELETE helpers;
  • a 30-second default request timeout;
  • caller-provided AbortSignal support;
  • typed ApiError values for HTTP, network, timeout, abort, and invalid-JSON failures; and
  • streamed SSE parsing with an idle watchdog for grounded answers.

Application routes

Route Purpose
/ Public Folio overview and session-aware entry action
/login Sign in
/sign-up Create an account
/spaces Search, sort, create, edit, and delete research spaces
/spaces/:id Redirect to the space's Sources tab
/spaces/:id/sources Browse, filter, add, edit, retry, and delete sources
/spaces/:id/sources/:sourceId Read normalized source content and cited passages
/spaces/:id/ask Browse and manage conversation history
/spaces/:id/ask/new Start a space- or source-scoped conversation
/spaces/:id/ask/:conversationId Replay and continue a conversation
/spaces/:id/notes Browse, create, edit, view, convert, and delete notes
/spaces/:id/notebook Edit and export the space notebook

/ask/new deliberately shares the same dynamic route segment as persisted conversations. Once the opening stream event returns a real conversation ID, the URL can change without unmounting the component that owns the active stream.


Backend API surface

All paths below are relative to API_BASE_URL, which defaults to /api/v1.

Domain Methods and paths used by this frontend
Authentication POST /auth/login, POST /auth/sign-up, POST /auth/refresh, POST /auth/logout, GET /users/me
Spaces GET/POST /spaces, GET/PATCH/DELETE /spaces/:id
Sources GET/POST /sources, GET/PATCH/DELETE /sources/:id, POST /sources/:id/retry, GET /sources/:id/preview, GET /sources/status (SSE)
Passages GET /passages/:id
Ask POST /spaces/:id/ask (SSE), GET /spaces/:id/ask/suggestions
Conversations GET /spaces/:id/conversations, GET/PATCH/DELETE /spaces/:id/conversations/:conversationId
Answer feedback POST /spaces/:id/conversations/:conversationId/messages/:messageId/feedback
Notes GET/POST /spaces/:id/notes, GET/PATCH/DELETE /spaces/:id/notes/:noteId, POST /spaces/:id/notes/:noteId/convert-to-source
Notebook GET/PUT /spaces/:id/notebook

Two backend URLs are reached without the API client, because a new tab or an <img> cannot carry a bearer header:

  • Open original. GET /sources/:id/preview returns a relative /api/v1/sources/:id/file?token=… link. The token is a 5-minute signed token scoped to that one source. The reader resolves the link against the page origin and opens it in a new tab.
  • Reader images. Images extracted from DOCX and EPUB sources load straight from /api/v1/sources/:id/media/:fileName.

Development commands

Run commands from the repository root.

Command Purpose
yarn dev Start the Next.js development server on port 3000
yarn build Create the optimized standalone production build
yarn start Serve an existing production build
yarn lint Run ESLint, Next.js rules, Prettier checks, and unused-import checks

Testing and quality checks

The repository currently has no automated test suite or checked-in CI workflow. Before opening a pull request, run:

yarn lint
yarn build

Both commands must finish without errors. ESLint also reports Prettier and unused-variable warnings, so review warnings rather than treating a zero exit code as the only quality signal.


Project conventions

  • Keep page.tsx files as Server Components. Move state, event handlers, effects, and browser APIs into sibling Client Components.
  • Import project modules through the @/* alias.
  • Keep domain API calls in *.service.ts; components should consume the higher-level use*.ts hooks.
  • Define stable query keys as as const tuples and invalidate related data after successful mutations.
  • Use React Hook Form with module-level Zod schemas and the shared form components under components/shared/ui/.
  • Use Sonner for mutation success and error feedback.
  • Define design tokens in app/globals.css; Tailwind CSS 4 is configured CSS-first and has no tailwind.config.ts.
  • Prefer Tailwind's numeric spacing scale to arbitrary pixel lengths.
  • Use the bundled Next.js 16 documentation under node_modules/next/dist/docs/ before changing framework-sensitive routing, caching, or server/client behavior.
  • Make focused changes and avoid refactoring unrelated code.

See AGENTS.md for the complete development guide.


Production

Folio runs at https://folio.nustechnology.com on the shared NUS Technology VPS. This repo is its own Compose project, folio-web, with one service, folio-frontend. The backend is a separate project from Folio-BE; the two meet only on the external shared-network.

Browser ──https──► Cloudflare            TLS terminates here; an Origin Rule
                      │                  sends the subdomain to VPS port 3000
                      ▼
                 shared-nginx :3000
                      ├── /            → folio-frontend:3000   (this repo)
                      ├── /api/        → folio-backend:3001    (Folio-BE)
                      └── /api-docs    → folio-backend:3001

Nginx serves the UI and the API from one hostname, so the default relative /api/v1 base works with no build args and no deployment hostname in the client bundle. A domain change needs no frontend rebuild. The token cookies are Secure in production, which works because Cloudflare serves the site over HTTPS.

Deploy

The clone lives at /home/nus/srv/folio/Folio-Web. Its .env holds one line, COMPOSE_FILE=docker-compose.prod.yml, so bare Compose commands select the production stack:

cd /home/nus/srv/folio/Folio-Web
git pull
docker compose up -d --build
docker compose ps
docker compose logs -f folio-frontend

A frontend deploy never restarts the API, the worker or the databases. docker-compose.prod.yml has no backend dependency, because browser API traffic reaches the backend through Nginx, not through frontend-container DNS. For the few seconds before the API is up, /api/ returns 502.

Outside the VPS, pass the file explicitly: docker compose -f docker-compose.prod.yml up -d --build.

Image

The production image is built in three stages. The first two share a base stage: Node.js 22 Alpine with Yarn 4.12.0 activated through Corepack. The runner starts from plain node:22-alpine, because the standalone server never calls Yarn.

  1. deps installs the immutable Yarn dependency tree.
  2. builder creates the Next.js standalone output.
  3. runner copies only the traced server, static assets, and public files into a non-root Node.js 22 Alpine image.

The container runs as uid 1001, listens on port 3000, publishes no host port, and includes an HTTP health check.

Deployment secrets and server access: contact SamHT.


Troubleshooting

Every API call returns 404 or 502

Confirm that the backend is running on http://localhost:3001. If it uses another origin, set API_PROXY_TARGET in .env.local and restart the development server.

Requests use the wrong host or fail as mixed content

Remove a stale NEXT_PUBLIC_API_URL from .env.local. The normal local and production topology uses the relative /api/v1 base.

Development assets are blocked through a tunnel or LAN address

ngrok and Cloudflare tunnel domains are preconfigured. Add any other host to ALLOWED_DEV_ORIGINS, then restart yarn dev.

Writes fail with a CORS error through a tunnel

Add the frontend tunnel origin to the backend's development CORS allowlist. The Next.js rewrite changes the destination host but preserves the browser's Origin header.

The session immediately expires or requests loop on 401

Inspect the responses from /auth/refresh and /users/me. Refresh requires a valid refresh-token cookie, and the frontend only retries a request automatically when the backend identifies the 401 as TOKEN_EXPIRED.

Source progress stops updating

Check the authenticated SSE request to /api/v1/sources/status. A valid access token is required; the client reconnects with exponential backoff and refreshes once after a 401.

Streamed answers stop before completion

Check the POST /api/v1/spaces/:id/ask response and backend logs. The client aborts a stream that produces no SSE frame for 60 seconds and surfaces the failure on the assistant message.


License

This project is licensed under the NUS Technology Non-Commercial License 1.0. Commercial use and redistribution are not permitted. For commercial licensing, contact NUS Technology.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages