Everything needed to build and run the Single Gateway of Thailand Life Sciences — a public web portal and analytics layer over a central life-sciences knowledge database, for TCELS (สทนว.). The system is structured as Web → API → Database; the staff admin CMS is a phase-2 addition.
Introduction
TILSNA presents researchers, companies, research, products, innovations and funding across five focus industries, with dashboards, smart search, detail views, graph analysis and supply-chain visualization. This guide is the engineering reference for the front-end web app, the REST API it consumes, and the PostgreSQL knowledge database behind it.
The platform is delivered against the project TOR and the central-database / API specification. Mandated non-functional standards: microservice architecture, On-Cloud deployment, a relational database (PostgreSQL / MySQL / MSSQL / Oracle), API security with OAuth 2.0 + JWT, WSS/WAS web-security standards, PDPA compliance, and ISO/IEC 29110 project management.
PHARMA_CELL Pharmaceuticals & Cell TherapyGENOMICS_PRECISION Genomics & Precision MedicineMED_DEVICE_AI Medical Devices & Medical AINUTRA_COSME Nutraceuticals & CosmeceuticalsDIGITAL_HEALTH Digital Health
Architecture
Three tiers, loosely coupled so each can scale and deploy independently. The web app never talks to the database directly — it only calls the versioned REST API, which enforces auth and shapes responses for the front-end.
Web → API → Database. Microservices behind an OAuth2/JWT gateway; PostgreSQL holds the entity + relationship model.
Web (front-end) — Astro + React islands, served at the edge on Cloudflare. Renders landing, dashboards, explore, detail, graph and auth UI.
API (services) — REST /api/v1 with a standard JSON envelope. Microservices: catalog/search, graph & supply-chain, auth (Thai ID), import, and (phase 2) admin/CMS — all behind an OAuth 2.0 / JWT gateway.
# Node 22.12 (see .nvmrc)
nvm use # or: install Node 22.12
npm install # install dependencies
npm run dev # local dev server → http://localhost:4330
npm run build # production build (Cloudflare adapter)
npm run preview # preview the production build
Configure the API base URL via an environment variable (e.g. PUBLIC_API_BASE_URL=https://api.tilsna…/api/v1). Deployment is On-Cloud; this front-end ships to Cloudflare via a build/deploy hook.
Today the front-end reads mock JSON in src/data that mirrors the API contract, so swapping to the live API is a per-endpoint change with no UI rewrite.
Database schema
The database is an entity + relationship model, not a flattened spreadsheet. Twelve core tables in three groups: master data (the entities shown in the UI), a relationship layer (one row per typed edge, powering graph & supply-chain) and data governance (provenance + import staging).
Core knowledge tables — master data + entity_relationships + evidence_sources / staging. The accounts & operations tables are listed below.
The TOR requires authentication, role requests with staff/admin approval, a landing CMS, analytics and an audit trail — so the schema adds a platform layer of 11 tables alongside the knowledge data. See the data dictionary for every column.
-- Accounts, access & operations (TOR 3.5–3.10)
CREATE TABLE users (
user_id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE,
password_hash VARCHAR(255), -- null for Thai-ID-only accounts
thai_id_sub VARCHAR(100) UNIQUE, -- subject id from Thai ID (DOPA)
full_name VARCHAR(255),
phone VARCHAR(50),
line_id VARCHAR(100),
status VARCHAR(50) DEFAULT 'active', -- active / suspended / pending
email_verified BOOLEAN DEFAULT FALSE,
last_login_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE roles (
role_id SERIAL PRIMARY KEY,
code VARCHAR(50) UNIQUE NOT NULL, -- guest / researcher / company_rep / tcels_staff / admin
name_th VARCHAR(100), name_en VARCHAR(100), description TEXT
);
CREATE TABLE user_roles (
user_role_id SERIAL PRIMARY KEY,
user_id INT REFERENCES users(user_id),
role_id INT REFERENCES roles(role_id),
granted_by INT REFERENCES users(user_id),
granted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE (user_id, role_id)
);
CREATE TABLE role_requests (
request_id SERIAL PRIMARY KEY,
user_id INT REFERENCES users(user_id),
requested_role VARCHAR(50) NOT NULL, -- researcher / company_rep
status VARCHAR(50) DEFAULT 'pending', -- pending / approved / rejected
academic_position VARCHAR(255), orcid VARCHAR(100),
organization_name VARCHAR(255), research_interest TEXT, -- researcher fields
company_name VARCHAR(255), registration_no VARCHAR(100),
company_objective TEXT, registered_capital VARCHAR(100), directors TEXT, -- company fields
reviewer_id INT REFERENCES users(user_id),
review_note TEXT, decided_at TIMESTAMP,
linked_entity_type VARCHAR(50), linked_entity_id INT, -- set on approval
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE company_members ( -- company representatives (3.8.2)
company_member_id SERIAL PRIMARY KEY,
company_id INT REFERENCES companies(company_id),
user_id INT REFERENCES users(user_id),
member_role VARCHAR(50) DEFAULT 'representative', -- owner / representative
added_by INT REFERENCES users(user_id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE (company_id, user_id)
);
CREATE TABLE password_reset_tokens ( -- email password reset (3.5.3)
token_id SERIAL PRIMARY KEY,
user_id INT REFERENCES users(user_id),
token_hash VARCHAR(255) NOT NULL,
expires_at TIMESTAMP NOT NULL, used_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE content_articles ( -- landing CMS: news & funding (3.9.3 / 3.10.2)
article_id SERIAL PRIMARY KEY,
type VARCHAR(50) NOT NULL, -- news / funding_announcement
title_th VARCHAR(255), title_en VARCHAR(255), slug VARCHAR(255),
industry_category_id INT REFERENCES industry_categories(industry_category_id),
body TEXT, cover_image_url TEXT,
status VARCHAR(50) DEFAULT 'draft', -- draft / published / archived
author_id INT REFERENCES users(user_id), published_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE documents ( -- uploads: company certificate, certs (3.6.3)
document_id SERIAL PRIMARY KEY,
owner_type VARCHAR(50) NOT NULL, owner_id INT NOT NULL, -- role_request / company / researcher
doc_type VARCHAR(100), file_name VARCHAR(255), file_url TEXT,
mime_type VARCHAR(100), uploaded_by INT REFERENCES users(user_id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE entity_views ( -- view / visit analytics (3.7.1.5 / 3.10.6)
view_id BIGSERIAL PRIMARY KEY,
entity_type VARCHAR(50) NOT NULL, entity_id INT,
user_id INT REFERENCES users(user_id), -- null for guests
session_id VARCHAR(100), viewed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE entity_metrics ( -- derived rankings / citations / growth (3.7 / 3.8)
metric_id SERIAL PRIMARY KEY,
entity_type VARCHAR(50) NOT NULL, entity_id INT NOT NULL,
period VARCHAR(20), -- 2026-05 / all-time
citation_count INT DEFAULT 0, view_count INT DEFAULT 0,
rank_overall INT, computed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE audit_log ( -- database change history (3.10.6.8)
audit_id BIGSERIAL PRIMARY KEY,
user_id INT REFERENCES users(user_id),
action VARCHAR(50), -- create / update / delete / approve / reject / publish
entity_type VARCHAR(100), entity_id INT,
changes JSONB, ip_address VARCHAR(64),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Data dictionary
Every table in the schema, grouped. Entities carry verification_status where relevant and link out to evidence_sources for traceability. Jump to a table:
All connections live in a single entity_relationships table — one typed, directional row per edge with an optional evidence reference. The same rows drive both the force-directed graph analysis and the layered supply-chain view.
Value-chain layers: research → innovation → product → company → funding, with researchers, tags and industries as contributors.
Relationship types: affiliated_with · has_expertise · works_on · derived_to · owned_by / distributed_by · commercialized_by · funded_by · classified_as · related_to. Start with the single table; split into bridge tables (e.g. company_product) later for performance.
Import pipeline
Source data arrives as Excel. It lands in staging tables first, then is validated, mapped to the industry taxonomy, de-duplicated, and committed to master + relationship tables — never written straight into production.
POST /api/v1/import/excel → { import_batch_id, sheets_detected[] }
POST /api/v1/import/{id}/validate → { valid_rows, warning_rows, error_rows, warnings[] }
POST /api/v1/import/{id}/commit → { created{}, updated{}, skipped{ duplicate_companies } }
Mock data
The front-end ships with mock JSON under src/data/ that mirrors the API contract, so the whole UI runs before the live API exists — swapping to the real API is a per-endpoint change with no UI rewrite. Expand a dataset to browse it as a readable table or toggle to the raw JSON.
Download SQL
Two ready-to-run database packages — schema + sample data + media files, zipped. Each unzips to /sql (DDL + seed inserts) and /medias/<entity>/* (images keyed to each row's id). Built with PostgreSQL; portable to MySQL / MSSQL / Oracle.
Regenerate after data changes with bash scripts/build-sql-packages.sh (writes both zips to public/sql-package/). Sample password hashes are placeholders — replace before any real use.
API reference
REST under a single version, https://api.…/api/v1. Every response uses the same envelope so the front-end handles success, lists and errors uniformly.
Identity supports Thai ID (DOPA) and email/password; password reset is email-based. The API is protected with OAuth 2.0 + JWT, and the platform must meet TCELS security requirements.
Access control — role-based; least privilege per endpoint.
Encryption — TLS in transit; encryption at rest for sensitive data.
Web standards — Website Security Standard (WSS) & Web Application Security Standard (WAS).
PDPA — Thailand Personal Data Protection Act B.E. 2562: consent, data-subject rights, 30-day response.
Sign-up captures email, password, name and a contact channel (phone / LINE ID); the front-end sign-in, forgot-password and reset-password flows are already built.
Roles, journey & approval
Five roles — guest, researcher, company representative, TCELS staff and admin. A guest signs up, optionally requests an elevated role (researcher or company rep), and staff/admin review and approve it; on approval the user is linked to a researcher/company record and gets a personalized workspace. Staff/admin accounts are created by an administrator only.
From guest → sign-up → role request → staff/admin review → granted workspace, with the tables each step writes to.
Phase 1 — public web portal: landing, overview dashboards, smart search/filter, detail views, graph & supply-chain, auth + role-request UI.
Phase 2 — staff/admin CMS: content management, company-role approval with DBD auto-fill, user management, exportable reports (Word/PDF), admin analytics; full live API + database.
Timeline: March–May 2026, delivered within 90 days across three milestones (10% / 60% / 30%), per the project TOR.
Sources: core_database.pdf (schema + API spec) · Update TOR 2.5.pdf (requirements). Diagram assets & regeneration prompts live in public/developer/.