Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GTA-DC — Grounded Theory Curation Application

A three-layer TypeScript application that operationalises the Grounded Theory Curation Model (Adorjan, 2025). Researchers can create investigations, register their team, define the research process, build a grounded theory from codes and categories, and curate every artefact produced along the way.

The data model mirrors the PlantUML diagrams from the thesis (../Material/Diagrams/v2/) one-to-one, plus the minimum scaffolding needed to make the model usable in practice (Researcher accounts, ResearcherGroups, and a top-level Research container).


Table of contents

  1. Quick start
  2. Architecture
  3. Domain model
  4. Authentication & authorization
  5. Backend
  6. Frontend
  7. Configuration
  8. Development without Docker
  9. Project layout
  10. Verification
  11. Troubleshooting

Quick start

Requirements

  • Docker 24+ with the Compose plugin (docker compose ...)
  • ~1 GB of free disk for images and the Postgres volume

Boot the stack

cd "Taller de Iniciación Académica/app"
./app up

The first run takes ~1–2 minutes (image pulls + Prisma client generation). Subsequent runs are seconds.

URL What
http://localhost:5173 Frontend (React SPA)
http://localhost:3000 Backend REST API base
http://localhost:3000/api/docs Swagger UI for the full API
http://localhost:3000/health Liveness + DB probe

Other commands

./app up --reset      # drop all data and start fresh
./app up --seed       # load the Argonauts of the Western Pacific demo dataset
./app up --reset --seed  # clean DB + load the demo (recommended for first boot)
./app down            # stop the stack (data is preserved)
./app logs backend    # follow logs from a single service
./app help            # all options

The script auto-creates .env from .env.example on first run.

Choosing the AI provider

The AI features can run against Watsonx.ai (IBM cloud, the default) or Ollama (local models). You pick the provider when you start the stack:

./app up                      # Watsonx.ai (default — nothing to install locally)
./app up --provider ollama    # Ollama, running models on your machine
  • Watsonx (default). Set AI_ENABLED=1 and your WATSONX_API_KEY / WATSONX_PROJECT_ID in .env. No Ollama containers are built or started, so a modest machine has zero extra overhead.

  • Ollama (local). --provider ollama enables AI, starts a local ollama container, and a one-shot ollama-pull container that downloads the default Granite model (granite3.3:8b) the first time — this can take a while and several GB. Track the download with:

    ./app logs ollama-pull

    Granite is used for every AI use case by default; override with OLLAMA_MODEL_ID in .env. Requirements: enough RAM/disk for an 8B model (~6–8 GB). The model is cached in a Docker volume, so later starts are fast.

Either way the active provider and its models are reported at GET /api/v1/ai/status, and the UI's model selectors adapt automatically.


Architecture

┌────────────────────────────────────────────────────────────────┐
│  Browser  ─▶  http://localhost:5173                            │
│  ────────                                                       │
│                                                                 │
│  ┌──────────────┐   /api/* (proxied)   ┌──────────────┐         │
│  │  Frontend    │ ──────────────────▶  │  Backend     │         │
│  │  React SPA   │                      │  NestJS API  │         │
│  │  nginx       │                      │  Node 20     │         │
│  └──────────────┘                      └──────┬───────┘         │
│                                                │ Prisma         │
│                                                ▼                │
│                                         ┌──────────────┐        │
│                                         │  Postgres 16 │        │
│                                         └──────────────┘        │
└────────────────────────────────────────────────────────────────┘
Layer Stack
Database PostgreSQL 16
Backend NestJS 10 · Prisma 5 · JWT (passport-jwt) · class-validator · Swagger
Frontend React 18 · Vite · TypeScript · Tailwind CSS · TanStack Query · React Router · lucide-react
Container Docker Compose · multi-container (db, backend, frontend)

The frontend's nginx reverse-proxies /api/* and /health to the backend, so the browser only ever talks to a single origin.


Domain model

The schema (backend/prisma/schema.prisma) encodes Adorjan's GTA-DC model verbatim and adds three new entities to make it usable as software.

Mapping from PlantUML packages to Prisma models

PlantUML package Models Notes
01_Artefact Artefact (abstract base) Class-table inheritance root
02_ResearchContext ResearchContext, Field, Participant, TheoreticalFramework, Memo, Finding, Bibliography, ResearchQuestion, Method, Protocol, Tool Field is 1..n in Context; Method has many Protocols and many Tools
03_GroundedTheory Theory, Category, Code, CategoryRelation, CodeRelation M2M self-relations for "related categories/codes"
04_ResearchTeam ResearchTeam, ConsensusCriteria Researcher participates via ResearchTeamMembership
05_ResearchProcess Process, Stage, Activity, ActivityTool, ActivityInputArtefact, ActivityOutputArtefact Activity links to many tools and many input/output artefacts

New entities (added on top of the model)

Model Purpose
Researcher The PlantUML <<Actor>> Researcher extended with email, passwordHash, displayName, isAdmin. This is the user account.
ResearcherGroup + ResearcherGroupMembership Institutional grouping (e.g. "ORT Grounded Theory Lab"). Many-to-many with Researcher.
Research Top-level container that bundles a ResearchContext, a Theory, a Process, and a ResearchTeam. Has a single ownerId (a Researcher). One Researcher can own / participate in many Researches.

Class-table inheritance for Artefact

The PlantUML model marks 14 classes as <<Artefact>> (Theory, Category, Code, ResearchContext, Field, TheoreticalFramework, Memo, Finding, Bibliography, ResearchQuestion, Method, Tool, Protocol, ConsensusCriteria). They share hashID, name, media, access, status, content, anonymized, responsibleId, etc.

Rather than duplicating those columns 14 times, the schema uses class-table inheritance:

  • A base Artefact table holds every shared field plus a kind discriminator (ArtefactKind enum).
  • Each subtype gets its own table whose primary key is also a foreign key back to Artefact.id (cascade delete). The subtype table holds only the fields specific to that subtype (e.g. Theory.theoryType, ResearchQuestion.identifier, Tool.referenceURL).

This buys two things:

  1. Strong typing per subtype — Prisma still sees Theory.theoryType, Code.categoryId, etc.
  2. Uniform queries — GET /researches/:id/artefacts hits a single base table and returns every artefact regardless of subtype, which the frontend uses for the "All artefacts" tab.

Service code maintains the invariant inside transactions: every artefact create/delete touches both the base row and the subtype row atomically.

Enums

TypeOfMedia    : video | text | audio | dataset | software
TypeOfAccess   : public | private
TypeOfStatus   : pending | inprogress | complete
TypeOfRole     : Junior | Senior          (academic role on Researcher)
TypeOfVote     : unanimous | majority | consensus  (ConsensusCriteria)
TypeOfTheory   : classic | constructivist | straussian
TypeOfMethod   : qualitative | quantitative | mixed
ArtefactKind   : THEORY, CATEGORY, CODE, RESEARCH_CONTEXT, FIELD,
                 THEORETICAL_FRAMEWORK, MEMO, FINDING, BIBLIOGRAPHY,
                 RESEARCH_QUESTION, METHOD, TOOL, PROTOCOL,
                 CONSENSUS_CRITERIA

Authentication & authorization

Authentication

  • POST /api/v1/auth/register — creates a Researcher and returns { researcher, token }.
  • POST /api/v1/auth/login — returns { researcher, token }.
  • GET /api/v1/auth/me — returns the current researcher.

The token is a JWT signed with JWT_SECRET (default 7d lifetime). The frontend stores it in localStorage under gtadc_token and an Axios-like fetch wrapper attaches it to every request.

A global JwtAuthGuard protects every route. Public routes are explicitly opted in with the @Public() decorator (currently: auth/register, auth/login, health).

Authorization model

Three orthogonal layers:

Layer Mechanism
System admin Researcher.isAdmin boolean. Admins can create/edit any ResearcherGroup and act as the owner of any Research.
Per-research owner Research.ownerId. The owner can edit/delete the research and any of its artefacts. Enforced by a per-service helper ResearchAccessService.assertOwnable(...).
Per-research membership A researcher who is on the ResearchTeam (via ResearchTeamMembership) can read everything in that research but cannot mutate anything (unless they are also owner or admin).

Academic role (Junior / Senior from the GTA-DC model) is not used for authorization — it's metadata for the team page.

Auto-team-creation

When a Research is created, the backend transactionally:

  1. Creates the Research row with ownerId = currentUser.id.
  2. Creates an empty ResearchTeam 1-1 with that research.
  3. Adds the owner as the first team member.

So a freshly-created research is always immediately editable by its owner and appears in their dashboard.


Backend

Folder layout (one module per concern)

backend/src/
├── main.ts                       # bootstrap, validation, Swagger
├── app.module.ts                 # wires every feature module
├── prisma/                       # PrismaService (global)
├── common/                       # ResearchAccessService, base DTOs
├── auth/                         # JWT strategy, guards, /auth/* endpoints
├── researcher/                   # /researchers (user CRUD)
├── researcher-group/             # /researcher-groups
├── research/                     # /researches  (top-level container)
├── research-team/                # team members + ConsensusCriteria
├── research-context/             # Context + Field + Participant
├── theoretical-framework/        # Framework + Memo, Finding, Bibliography,
│                                 # ResearchQuestion, Method, Protocol, Tool
├── grounded-theory/              # Theory + Category + Code (+ M2M relations)
├── research-process/             # Process + Stage + Activity (+ M2M)
├── artefact/                     # cross-cutting GET /researches/:id/artefacts
└── health/                       # GET /health

The five PlantUML packages (research-context, theoretical-framework, grounded-theory, research-team, research-process) each become a NestJS module of the same name, so the directory structure visibly mirrors the model.

Endpoints overview

The full API is documented at http://localhost:3000/api/docs (Swagger UI, 61 endpoints). Highlights:

Group Notable routes
auth POST /auth/register, POST /auth/login, GET /auth/me
researchers GET/PATCH/DELETE /researchers[/:id]
researcher-groups GET/POST/PATCH/DELETE /researcher-groups, POST/DELETE …/members
researches GET/POST/PATCH/DELETE /researches[/:id]
team …/team, …/team/members, …/team/consensus-criteria, /consensus-criteria/:id
context …/context, /contexts/:id/fields, /fields/:id/participants
framework /contexts/:id/framework and CRUD for memos, findings, bibliographies, research-questions, methods, protocols, plus method↔tool linking
tools /researches/:id/tools, /tools/:id
grounded-theory /researches/:id/theory, /theories/:id/categories, /categories/:id/codes, plus /relations for self-M2M
process /researches/:id/process, /processes/:id/stages, /stages/:id/activities, plus tools/inputs/outputs M2M
artefact GET /researches/:id/artefacts?kind=… (uniform across all subtypes)

Validation & error model

  • DTOs use class-validator with whitelist + forbidNonWhitelisted so unknown fields are rejected early.
  • ValidationPipe is global. Validation errors return HTTP 400 with a human-readable message array.
  • Auth failures return 401 (token missing/expired/invalid).
  • Authorization failures return 403.
  • Missing entities return 404.
  • All errors share NestJS's standard envelope: { statusCode, message, error }.

Prisma & migrations

  • Schema lives in backend/prisma/schema.prisma.
  • Initial migration is committed under backend/prisma/migrations/.
  • On container start the backend runs prisma migrate deploy, then starts Nest. With RESET=1 it runs prisma migrate reset --force --skip-seed first, wiping the database before re-applying migrations.

Notable backend files

  • backend/prisma/schema.prisma — encodes the entire model.
  • backend/src/common/artefact-base.dto.tsArtefactBaseDto shared by every artefact create-DTO; pickArtefactFields() and pickArtefactUpdateFields() are the helpers that feed the Prisma create / update calls in subtype services.
  • backend/src/common/research-access.tsassertReadable() and assertOwnable() enforce per-research authorization. Used by every feature service.
  • backend/src/auth/research-owner.guard.ts — per-route guard for research-scoped mutating endpoints (kept available for future use; today most endpoints call assertOwnable from inside the service so the rule is next to the data).

Frontend

Tech & design

  • React 18 with React Router v6.
  • TanStack Query for all server state. Mutations call qc.invalidateQueries(...) and the relevant lists refresh automatically.
  • Tailwind CSS with a custom ink (slate-derived) and accent (violet) palette, plus Inter for body and Source Serif Pro for headings — a modern academic look.
  • lucide-react for icons.
  • A small in-house design system (src/components/): Button, Input / Textarea / Select / Field, Card, Modal, ConfirmDialog, Badge / StatusBadge, Avatar, EmptyState, Tabs, Toast, PageHeader, TopBar, Layout.

Routes

Path Page
/login Sign in (with hero quote from Malinowski's Argonauts)
/register Create account
/ Dashboard (stats, recent researches, your groups)
/researches All researches with create modal
/researches/:id Research detail with six sticky tabs
/groups Researcher groups (admin manages, others view)
/profile Edit your own account

Research detail tabs

The Research detail page is the primary workspace. Tabs are sticky and update their counts live:

Tab What it manages
Overview Title/description, owner, component status (which of Context/Theory/Process/Team are configured), danger zone (delete)
Context ResearchContext (1-1), Field (1..n) with Participant chips, TheoreticalFramework (1-1) with five inline blocks for ResearchQuestion, Method (with nested Protocol rows), Memo, Finding, Bibliography, plus a research-scoped Tool registry
Team Members with role/owner badges, add/remove. Consensus criteria as Artefacts with voting type
Process Process (1-1), Stage (1..n), Activity (1..n) with status, dates, responsible researcher (drawn from team)
Grounded Theory Theory with variant (classic/constructivist/straussian), Category (1..n), Code (n..n inside categories)
All artefacts Uniform read-only list pulled from GET /researches/:id/artefacts, filterable by ArtefactKind

Auth flow

  • AuthProvider (src/auth/AuthContext.tsx) reads the JWT from localStorage on mount, calls GET /auth/me, and exposes { user, login, register, logout, refresh }.
  • <ProtectedRoute> wraps the authenticated app. Unauthenticated requests are redirected to /login with state.from so post-login lands you back where you started.
  • Any 401 response from the API client clears the token and triggers a redirect on the next render.

State updates after mutations

A consistent pattern across the codebase:

const qc = useQueryClient();
const toast = useToast();
const mut = useMutation({
  mutationFn: () => api.something.create(...),
  onSuccess: () => {
    qc.invalidateQueries({ queryKey: ['something', parentId] });
    toast.success('Saved');
  },
  onError: (err) =>
    toast.error(err instanceof ApiError ? err.message : 'Could not save'),
});

Configuration

The Compose stack reads app/.env (auto-bootstrapped from .env.example).

Variable Default Purpose
POSTGRES_USER gtadc DB user
POSTGRES_PASSWORD gtadc DB password
POSTGRES_DB gtadc DB name
POSTGRES_PORT 5432 Host port for Postgres
JWT_SECRET change-me-in-production Set this in any real deployment
JWT_EXPIRES_IN 7d JWT lifetime
BACKEND_PORT 3000 Host port for the backend
FRONTEND_PORT 5173 Host port for the frontend
RESET 0 Set to 1 (or use ./app up --reset) to wipe DB
SEED 0 Set to 1 (or use ./app up --seed) to load the Argonauts demo
AI_ENABLED 0 Master switch for all AI features
AI_PROVIDER watsonx Active provider: watsonx or ollama (set by ./app up --provider)
WATSONX_API_KEY (empty) IBM Cloud API key (Watsonx)
WATSONX_PROJECT_ID (empty) Watsonx project id
WATSONX_MODEL_ID ibm/granite-4-h-small Fallback model when a request sends no override
WATSONX_URL https://us-south.ml.cloud.ibm.com Watsonx region endpoint
OLLAMA_BASE_URL http://ollama:11434 Ollama server URL (in-compose service)
OLLAMA_MODEL_ID granite3.3:8b Granite model used for every AI use case
OLLAMA_PORT 11434 Host port for the Ollama service

The backend image also reads its own .env if you run it locally outside Docker (see Development without Docker).


Development without Docker

If you want hot reload + fast iteration on either layer:

Backend

cd backend
npm install
cp .env.example .env             # tweak DATABASE_URL if needed
npx prisma migrate dev           # run migrations (against a Postgres you control)
npm run start:dev                # nest start --watch on http://localhost:3000

For the database, the easiest path is to leave ./app up running for just the db service:

docker compose up -d db
DATABASE_URL='postgresql://gtadc:gtadc@localhost:5432/gtadc?schema=public' npm run start:dev

Frontend

cd frontend
npm install
npm run dev                      # vite on http://localhost:5173

The Vite dev server proxies /api and /health to process.env.VITE_API_PROXY ?? 'http://localhost:3000' (see vite.config.ts), so the frontend can talk to either the dockerised backend or a npm run start:dev backend without changes.

Generating a new migration

cd backend
docker compose -f ../docker-compose.yml up -d db          # if not already running
DATABASE_URL='postgresql://gtadc:gtadc@localhost:5432/gtadc?schema=public' \
  npx prisma migrate dev --name describe-the-change

Commit the new folder under backend/prisma/migrations/. On the next ./app up, the backend container will run prisma migrate deploy automatically.


Project layout

app/
├── README.md                                  ← this file
├── app                                        # entrypoint script
├── docker-compose.yml
├── .env.example
├── backend/
│   ├── Dockerfile
│   ├── package.json
│   ├── tsconfig.json / tsconfig.build.json
│   ├── nest-cli.json
│   ├── prisma/
│   │   ├── schema.prisma                      ← the entire data model
│   │   └── migrations/
│   └── src/
│       ├── main.ts / app.module.ts
│       ├── prisma/                            # PrismaService (global)
│       ├── common/                            # base DTOs, ResearchAccessService
│       ├── auth/                              # JWT, guards, decorators
│       ├── researcher/                        # /researchers
│       ├── researcher-group/                  # /researcher-groups
│       ├── research/                          # /researches
│       ├── research-team/                     # team members + consensus
│       ├── research-context/                  # context + field + participant
│       ├── theoretical-framework/             # framework + 7 sub-artefacts
│       ├── grounded-theory/                   # theory + category + code
│       ├── research-process/                  # process + stage + activity
│       ├── artefact/                          # cross-cutting list endpoint
│       └── health/                            # GET /health
└── frontend/
    ├── Dockerfile
    ├── nginx.conf                             # SPA fallback + API proxy
    ├── package.json
    ├── tsconfig.json / tsconfig.app.json / tsconfig.node.json
    ├── vite.config.ts
    ├── tailwind.config.js
    ├── postcss.config.js
    ├── index.html
    └── src/
        ├── main.tsx / App.tsx / index.css
        ├── lib/                               # api client + types + query
        ├── auth/                              # AuthProvider + ProtectedRoute
        ├── components/                        # design system primitives
        ├── pages/
        │   ├── LoginPage.tsx / RegisterPage.tsx
        │   ├── DashboardPage.tsx
        │   ├── ResearchesPage.tsx
        │   ├── ResearchDetailPage.tsx         # tab orchestrator
        │   ├── ResearcherGroupsPage.tsx
        │   └── ProfilePage.tsx
        └── features/
            └── research/                      # one file per detail tab
                ├── OverviewTab.tsx
                ├── ContextTab.tsx
                ├── TeamTab.tsx
                ├── ProcessTab.tsx
                ├── GroundedTheoryTab.tsx
                └── ArtefactsTab.tsx

Verification

A clean smoke test from a freshly cloned repo:

cd "Taller de Iniciación Académica/app"
./app up --reset

Then open http://localhost:5173, register a new account, create a Research, and walk through the six tabs:

  1. Overview — verify the auto-created team has you as the only member
  2. Context — Create context → add a Field → add a few Participants → create the Theoretical Framework → add a Research Question, a Method with a Protocol, a Memo
  3. Team — invite a second account (register another in another browser) → confirm membership is visible to both
  4. Process — Create process → add a Stage → add an Activity with a responsible researcher
  5. Grounded Theory — Create theory → add a Category → add a Code
  6. All artefacts — confirm each artefact you created shows up here, and that the kind filter chips work

For pure-API verification, the Swagger UI at http://localhost:3000/api/docs lets you exercise every endpoint with a token pasted into the Authorize box.


Troubleshooting

docker: unknown command: docker compose You have Docker but not the Compose v2 plugin. On macOS, the Homebrew formula docker-compose ships the plugin — symlink it so the CLI finds it:

mkdir -p ~/.docker/cli-plugins
ln -sf /opt/homebrew/lib/docker/cli-plugins/docker-compose ~/.docker/cli-plugins/docker-compose
docker compose version   # should now print 2.x

Port 3000 / 5173 / 5432 already in use Edit .env and set BACKEND_PORT, FRONTEND_PORT or POSTGRES_PORT, then ./app down && ./app up.

The backend container restarts in a loop Tail the logs: ./app logs backend. The most common causes are the DB not being healthy yet (Compose waits, but if you killed Postgres mid-flight the restart can race) and an out-of-date Prisma client. Try ./app down -v && ./app up --reset.

"Email already registered" on the very first registration Either the DB has stale data from a previous run, or you registered twice. Use ./app up --reset to start over.

Frontend shows "Loading…" forever Check the browser network tab. If GET /auth/me is returning 401, your token is invalid or the backend is unreachable; clear localStorage and reload, then sign in again.

What does --seed do? It loads a fully populated demo Research based on Bronisław Malinowski's Argonauts of the Western Pacific (1922). Three Researcher accounts, an institutional group, ~50 artefacts across every ArtefactKind, a four-stage process, and a five-category theory are created. See DEMO.md for a self-guided tour of the seeded data, and the seed source at backend/src/seed/malinowski.ts for the exact mapping from the ethnography to GTA-DC entities. Re-running --seed always wipes and re-creates the seeded research and users; other accounts you have added are preserved.


Reference

  • Adorjan, A. (2025). Modelling the Curation of Grounded Theory Research Artefacts. Doctoral thesis, Universidad ORT Uruguay / PEDECIBA. Source diagrams: ../Material/Diagrams/v2/. Summary: ../Material/Resumen_Tesis_Adorjan.md.
  • Glaser, B. & Strauss, A. (1967). The Discovery of Grounded Theory.
  • Charmaz, K. (2006). Constructing Grounded Theory.
  • Malinowski, B. (1922). Argonauts of the Western Pacific. — used as the worked-example case study (and the quote on the login screen).

About

GTA-DC — Grounded Theory Curation Application: three-layer TypeScript app (NestJS + Prisma + React/Vite + Docker) operationalising the Grounded Theory Curation Model (Adorjan, 2025).

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages