Server Guide¶
This guide covers the IFClite server architecture for production deployments with intelligent caching, parallel processing, and streaming.
Overview¶
The IFClite server processes IFC files on a high-performance Rust backend, providing:
- Content-Addressable Caching - Same file = instant response (skip upload entirely)
- Parallel Processing - Multi-core geometry extraction with Rayon
- Parquet Format - roughly 15-50x smaller payloads than JSON
- SSE Streaming - Progressive geometry for immediate rendering
- Full Data Model - Properties, quantities, and spatial hierarchy computed upfront
Client-Side WASM Is Now the Default
As of the latest release, client-side WASM parsing is the default processing mode. The server is no longer required for basic usage and must be explicitly opted into. Use the server when you need shared caching, parallel processing for very large files, or team-wide deployments.
When to Use Server vs Client¶
| Scenario | Recommendation |
|---|---|
| Single file, one-time view | Client-only (@ifc-lite/parser) |
| Team sharing same files | Server with caching |
| Large models (100+ MB) | Server with streaming |
| Repeat access to same files | Server with caching |
| Offline/embedded apps | Client-only |
| Privacy-sensitive data | Client-only |
Architecture¶
Quick Start¶
1. Start the Server¶
2. Connect from Client¶
import { IfcServerClient } from '@ifc-lite/server-client';
const client = new IfcServerClient({
baseUrl: 'http://localhost:3001'
});
// Health check
const health = await client.health();
console.log('Server status:', health.status);
3. Parse a File¶
// Parquet format (~15x smaller than JSON)
const result = await client.parseParquet(file);
console.log(`Meshes: ${result.meshes.length}`);
console.log(`Cache key: ${result.cache_key}`);
console.log(`From cache: ${result.stats.from_cache}`);
API Endpoints¶
Parse Endpoints¶
| Endpoint | Method | Description |
|---|---|---|
/api/v1/parse |
POST | Full parse, JSON response |
/api/v1/parse/parquet |
POST | Full parse, Parquet response (~15x smaller) |
/api/v1/parse/parquet/optimized |
POST | Optimized Parquet (~50x smaller) |
/api/v1/parse/stream |
POST | Streaming JSON (SSE) |
/api/v1/parse/parquet-stream |
POST | Streaming Parquet (SSE) |
/api/v1/parse/metadata |
POST | Quick metadata only (no geometry) |
All parse endpoints that return geometry also surface the 2D symbol stream
(IfcAnnotation + IfcGrid), matching @ifc-lite/parse. The JSON and SSE
responses carry it inline as symbolic_data (in the complete event for the
streaming variants); the binary Parquet transports expose it by cache key via
/api/v1/parse/symbolic/{key} (see below).
Every geometry endpoint's ModelMetadata carries length_unit_scale (factor to
convert model length values to metres, e.g. 0.001 for millimetres) and, when
the model has an IfcMapConversion / IfcProjectedCRS, a georeferencing
object (CRS name, datum, false eastings/northings, orthogonal height, grid-north
rotation, and a local→map 4×4 matrix) — matching @ifc-lite/parse. For the
JSON/SSE endpoints it's on metadata; for the Parquet endpoints it's in the
X-IFC-Metadata header.
Cache Endpoints¶
| Endpoint | Method | Description |
|---|---|---|
/api/v1/cache/check/{hash} |
GET | Check if file is cached (200 or 404) |
/api/v1/cache/geometry/{hash} |
GET | Fetch cached geometry (no upload) |
/api/v1/cache/{key} |
GET | Retrieve cached JSON result |
/api/v1/parse/data-model/{key} |
GET | Fetch cached data model |
/api/v1/parse/symbolic/{key} |
GET | Fetch 2D symbol data (IfcAnnotation + IfcGrid) as JSON |
Utility Endpoints¶
| Endpoint | Method | Description |
|---|---|---|
/ |
GET | API information |
/api/v1/health |
GET | Health check (liveness; always open) |
/api/v1/ready |
GET | Readiness probe (503 while the memory breaker is shedding load) |
/api/v1/metrics |
GET | Prometheus text metrics (registered only when IFC_METRICS_ENABLED=1) |
Optional bearer-token auth
When IFC_SERVER_API_TOKEN (or API_TOKEN) is set, all parse and cache
endpoints require an Authorization: Bearer <token> header and return 401
otherwise. The /, /api/v1/health, and /api/v1/ready probes stay open
so health checks keep working. When unset (the default), all routes are
open and the server logs a startup warning that it is unauthenticated.
Client SDK¶
IfcServerClient¶
import { IfcServerClient } from '@ifc-lite/server-client';
const client = new IfcServerClient({
baseUrl: 'http://localhost:3001',
timeout: 300000 // 5 minutes (default)
});
Parse Methods¶
parseParquet (Recommended)¶
Best for most use cases - roughly 15x smaller payloads than JSON.
const result = await client.parseParquet(file);
// Result contains:
// - cache_key: string
// - meshes: MeshData[]
// - metadata: ModelMetadata
// - stats: ProcessingStats
// - parquet_stats: { payload_size, decode_time_ms }
// - data_model?: ArrayBuffer (properties, quantities, hierarchy)
parseParquetOptimized¶
Roughly 50x smaller payloads using integer quantization (0.1mm precision).
const result = await client.parseParquetOptimized(file);
// Same as parseParquet but with:
// - Integer vertex quantization (0.1mm precision)
// - Byte colors (0-255 instead of 0-1)
// - Mesh deduplication (instancing)
parseParquetStream¶
Progressive rendering for large files (>50MB).
import type { MeshData as ServerMeshData } from '@ifc-lite/server-client';
// Server meshes use snake_case fields (express_id); map them into the
// renderer's camelCase MeshData shape before uploading.
const toRendererMesh = (m: ServerMeshData) => ({
expressId: m.express_id,
ifcType: m.ifc_type,
positions: m.positions,
normals: m.normals,
indices: m.indices,
color: m.color,
});
const streamResult = await client.parseParquetStream(file, (batch) => {
// Called for each geometry batch
renderer.addMeshes(batch.meshes.map(toRendererMesh));
});
// Or use async iterator
for await (const event of client.parseStream(file)) {
switch (event.type) {
case 'start':
console.log(`Processing ~${event.total_estimate} entities`);
break;
case 'batch':
renderer.addMeshes(event.meshes.map(toRendererMesh));
break;
case 'progress':
console.log(`${event.processed}/${event.total}`);
break;
case 'complete':
console.log(`Done in ${event.stats.total_time_ms}ms`);
break;
}
}
getMetadata¶
Quick metadata extraction without geometry processing.
const metadata = await client.getMetadata(file);
// Returns:
// - schema_version: string (e.g. 'IFC2X3', 'IFC4', 'IFC4X3')
// - entity_count: number
// - geometry_count: number
// - file_size: number
Cache Methods¶
Checking Cache Before Upload¶
parseParquet hashes the file client-side and checks the server cache
before uploading, so re-parsing the same file skips the upload automatically:
// Automatic: parseParquet computes the SHA-256 and does the cache check
// internally, returning the cached result without re-uploading on a hit.
const result = await client.parseParquet(file);
To retrieve a previously processed result later, keep its cache_key. Re-calling
parseParquet serves the geometry straight from the cache; fetch the cached data
model (properties + spatial hierarchy) with fetchDataModel:
// Geometry: re-calling parseParquet returns the cached result without re-upload.
const result = await client.parseParquet(file);
// Data model (properties + hierarchy) for a known cache key:
const dataModelBuffer = await client.fetchDataModel(result.cache_key);
getCached(key) is the lower-level lookup for the JSON parse() cache and
returns a ParseResponse; it is not the retrieval path for Parquet geometry.
Fetching Data Model¶
Properties and spatial hierarchy are computed in parallel and cached:
import { decodeDataModel } from '@ifc-lite/server-client';
const result = await client.parseParquet(file);
// Data model might still be processing
// Use polling to wait for it
const dataModel = await client.fetchDataModel(result.cache_key);
if (dataModel) {
const decoded = await decodeDataModel(dataModel);
console.log(`Entities: ${decoded.entities.size}`);
console.log(`Property sets: ${decoded.propertySets.size}`);
}
Fetching Symbolic Data¶
The JSON (parse) and streaming endpoints return the 2D symbol stream
(IfcAnnotation + IfcGrid) inline as symbolic_data. The binary Parquet
endpoints (parseParquet, parseParquetOptimized) can't carry it inline —
fetch it by cache key instead:
const result = await client.parseParquet(file);
const symbols = await client.fetchSymbolic(result.cache_key);
if (symbols) {
console.log(`Grid axes: ${symbols.grid_axes.length}`);
console.log(`Annotations: ${symbols.polylines.length} polylines, ${symbols.texts.length} labels`);
}
Utility Methods¶
// Health check
const health = await client.health();
// Uploads are gzip-compressed automatically by parse()/parseParquet(),
// so no manual compression step is needed.
// Check Parquet decoder availability
const available = await client.isParquetSupported();
Data Model¶
The server computes a complete data model including entities, property sets,
quantity sets, relationships, spatial hierarchy, and — matching @ifc-lite/parse
— per-element classifications (IfcClassificationReference), materials
(IfcMaterialLayerSet layers with metre thicknesses), and documents
(IfcDocumentReference). The latter three are exposed as flat, element-keyed
arrays on the decoded DataModel (classifications, materials, documents)
and decode to empty arrays when served by an older server/cache.
Entities¶
interface EntityMetadata {
entity_id: number;
type_name: string;
global_id?: string;
name?: string;
description?: string;
object_type?: string;
has_geometry: boolean;
}
Properties¶
interface PropertySet {
pset_id: number;
pset_name: string;
properties: Property[];
}
interface Property {
property_name: string;
property_value: string;
property_type: string;
}
Quantities¶
interface QuantitySet {
qset_id: number;
qset_name: string;
method_of_measurement?: string;
quantities: Quantity[];
}
interface Quantity {
quantity_name: string;
quantity_value: number;
quantity_type: string; // 'Area', 'Volume', 'Length', etc.
}
Spatial Hierarchy¶
interface SpatialHierarchy {
nodes: SpatialNode[];
project_id: number;
element_to_storey: Map<number, number>;
element_to_building: Map<number, number>;
element_to_site: Map<number, number>;
element_to_space: Map<number, number>;
}
interface SpatialNode {
entity_id: number;
parent_id: number;
level: number;
path: string;
type_name: string;
name?: string;
elevation?: number;
children_ids: number[];
element_ids: number[];
}
Parquet Format¶
The server uses Apache Parquet for efficient binary serialization.
Standard Format¶
- Mesh Table: express_id, ifc_type, vertex/index offsets, RGBA color
- Vertex Table: x, y, z (Float32), nx, ny, nz (Float32)
- Index Table: i0, i1, i2 (Uint32 triangle indices)
Optimized Format¶
- Instance Table: entity_id, ifc_type, mesh_index, material_index
- Mesh Table: Deduplicated unique geometries
- Material Table: Deduplicated RGBA colors (Uint8)
- Vertex Table: Quantized integers (0.1mm precision)
- Index Table: Triangle indices
Decoding on Client¶
import {
decodeParquetGeometry,
decodeOptimizedParquetGeometry,
decodeDataModel
} from '@ifc-lite/server-client';
// Standard Parquet
const meshes = await decodeParquetGeometry(parquetBuffer);
// Optimized Parquet (with vertex dequantization)
const optimizedMeshes = await decodeOptimizedParquetGeometry(parquetBuffer, 10000);
// Data model
const dataModel = await decodeDataModel(dataModelBuffer);
Caching Strategy¶
Content-Addressable Keys¶
Cache keys are derived from file content:
# {filter} is the opening filter (e.g. "default"); a non-default tessellation
# quality appends a "-q{level}" suffix after it
{SHA256}-{filter}-parquet-v4 # Geometry
{SHA256}-{filter}-parquet-metadata-v4 # Metadata header
{SHA256}-{filter}-datamodel-v2 # Properties & hierarchy
{SHA256}-{filter}-symbolic-v1 # 2D symbol stream
Cache Flow¶
Cache Benefits¶
| Scenario | Without Cache | With Cache |
|---|---|---|
| First load of a file | Full parse + geometry extraction | Full parse + geometry extraction |
| Repeat load of the same file | Full parse again | Serve pre-computed Parquet from disk |
| Upload | Always | Skipped entirely on a hit (hash check first) |
On a cache hit the server does no parsing at all: the response is a disk read plus network transfer, so repeat loads are typically orders of magnitude faster than the first load.
Server Configuration¶
Environment Variables¶
| Variable | Default | Description |
|---|---|---|
PORT |
8080 | Server port |
RUST_LOG |
info (+ debug for server/http spans) | Log filter (error, warn, info, debug, trace) |
MAX_FILE_SIZE_MB |
500 | Maximum upload size in MB |
WORKER_THREADS |
CPU cores | Parallel processing threads |
CACHE_DIR |
./.cache (/app/cache in Docker) |
Cache directory path |
REQUEST_TIMEOUT_SECS |
300 | Request timeout in seconds |
INITIAL_BATCH_SIZE |
100 | Streaming initial batch size |
MAX_BATCH_SIZE |
1000 | Streaming maximum batch size |
CACHE_MAX_AGE_DAYS |
7 | Cache retention in days |
CORS_ORIGINS |
localhost dev origins | Allowed CORS origins (comma-separated, * for all) |
IFC_SERVER_API_TOKEN |
unset | Optional bearer token for parse/cache routes (falls back to API_TOKEN) |
IFC_MAX_CONCURRENT_PARSES |
WORKER_THREADS |
Parse jobs admitted at once (CPU gate) |
IFC_MEM_BUDGET_MB |
70% of detected memory limit | Upload bytes admitted at once; 0 disables the memory gate |
IFC_ADMISSION_QUEUE_DEPTH |
2 x WORKER_THREADS |
Requests allowed to queue for an admission permit |
IFC_ADMISSION_QUEUE_TIMEOUT_SECS |
5 | Longest a queued request waits for a permit |
IFC_MEM_SHED_PCT |
85 | RSS percentage of the budget above which new parses are shed |
IFC_METRICS_ENABLED |
false | Expose GET /api/v1/metrics |
Docker Compose¶
version: '3.8'
services:
ifc-lite-server:
image: ghcr.io/ltplus-ag/ifc-lite-server:latest
ports:
- "3001:8080"
environment:
- RUST_LOG=info
- MAX_FILE_SIZE_MB=500
- WORKER_THREADS=8
- CACHE_MAX_AGE_DAYS=30
volumes:
- ifc-cache:/app/cache
volumes:
ifc-cache:
Adding Health Checks
For orchestration systems requiring health checks, the server exposes
GET /api/v1/health. If your runtime image includes curl or wget:
Production Deployment¶
For production, consider:
- Reverse Proxy - Use nginx or Traefik for SSL termination
- Persistent Cache - Mount a volume for the cache directory
- Resource Limits - Set memory/CPU limits based on expected file sizes
- Monitoring - Enable debug logging for troubleshooting
# Railway deployment
railway up
# Fly.io deployment
fly deploy
# Kubernetes
kubectl apply -f k8s/deployment.yaml
Streaming¶
Dynamic Batch Sizing¶
The server uses dynamic batch sizing for optimal streaming:
- Initial batch: 100 entities (fast first frame)
- Growth: Increases based on processing speed
- Maximum: 1000 entities per batch
Streaming Flow¶
Client-Side Streaming¶
import type { MeshData as ServerMeshData } from '@ifc-lite/server-client';
// Server meshes use snake_case fields; map to the renderer's shape.
const toRendererMesh = (m: ServerMeshData) => ({
expressId: m.express_id,
ifcType: m.ifc_type,
positions: m.positions,
normals: m.normals,
indices: m.indices,
color: m.color,
});
// Using callback
await client.parseParquetStream(file, (batch) => {
// batch.meshes are already decoded server MeshData
renderer.addMeshes(batch.meshes.map(toRendererMesh));
});
// Using async iterator
for await (const event of client.parseStream(file)) {
if (event.type === 'batch') {
renderer.addMeshes(event.meshes.map(toRendererMesh));
}
}
Performance Optimization¶
Server-Side¶
- Parallel Processing - Geometry and data model extracted concurrently
- Rayon Thread Pool - Utilizes all CPU cores
- Streaming Caching - Meshes accumulated during stream, cached at end
- Lazy Data Model - Client polls for data model while rendering geometry
Client-Side¶
- Hash Check First - Skip upload if file is cached
- Parquet Decoding - WASM-based decoder for fast parsing
- Progressive Rendering - Render batches as they arrive
- Background Polling - Fetch data model while geometry renders
Network¶
- Gzip Compression - Applied automatically on upload by
parse()/parseParquet() - Parquet Format - roughly 15-50x smaller than JSON
- SSE Streaming - No polling overhead
Error Handling¶
Server Errors¶
try {
const result = await client.parseParquet(file);
} catch (error) {
if (error.status === 413) {
console.error('File too large - increase MAX_FILE_SIZE_MB');
} else if (error.status === 408) {
console.error('Timeout - try streaming for large files');
} else if (error.status === 500) {
console.error('Server error:', error.message);
}
}
Streaming Errors¶
for await (const event of client.parseStream(file)) {
if (event.type === 'error') {
console.error('Stream error:', event.message);
break;
}
}
Connection Errors¶
try {
await client.health();
} catch (error) {
if (error.message.includes('ECONNREFUSED')) {
console.error('Server not running');
} else if (error.message.includes('timeout')) {
console.error('Server not responding');
}
}
Next Steps¶
- Parsing Guide - Client-side parsing details
- Rendering Guide - WebGPU rendering features
- API Reference - Complete API documentation
- Architecture - System design details