AR furniture visualization platform — view 3D furniture models in augmented reality via mobile cameras, QR codes, and an embeddable web component.
RefinedAR is an open-source web platform that lets users upload 3D furniture models and visualize them in augmented reality through their mobile camera. Key capabilities:
- Upload GLTF/GLB models and convert them to AR-ready formats (USDZ for iOS, GLB for Android)
- Generate QR codes that open the AR viewer on mobile
- Embed the AR viewer in any website via the
refined-ar-viewerweb component - Manage products and users through a full-featured dashboard
The project is a monorepo containing 6 repositories that work together as a complete platform.
- Quick Start (Docker)
- Using the Widget
- Self-Hosting / Production Deployment
- Manual Setup (Without Docker)
- Architecture
- Environment Variables
- Contributing
| Repository | Type | Port | Description |
|---|---|---|---|
refined-ar-backend |
NestJS API | 4000 | Core API server (auth, products, file storage) |
refined-ar-next-js |
Next.js | 3000 | Main dashboard application |
refined-ar-converter-api |
NestJS API | 5001 | 3D model conversion microservice |
refined-ar-converter-app |
Next.js | 3001 | Converter web UI |
web-component |
Stencil.js | — | Embeddable AR viewer widget |
refined-commons |
TypeScript | — | Shared types and configuration |
┌─────────────────────────────────────────────────────────────────┐
│ Frontend Layer │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ refined-ar-next-js │ │ refined-ar-converter-app │ │
│ │ (Port 3000) │ │ (Port 3001) │ │
│ └──────────┬──────────┘ └──────────────┬──────────────┘ │
└─────────────┼───────────────────────────────┼──────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Backend Layer │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ refined-ar-backend │ │ refined-ar-converter-api │ │
│ │ (Port 4000) │ │ (Port 5001) │ │
│ └──────────┬──────────┘ └──────────────┬──────────────┘ │
└─────────────┼───────────────────────────────┼──────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Infrastructure Layer │
│ ┌────────────┐ ┌────────────┐ ┌────────────────────────────┐ │
│ │ PostgreSQL │ │ Redis │ │ AWS S3 │ │
│ │ (54321) │ │ (6379) │ │ (File Storage) │ │
│ └────────────┘ └────────────┘ └────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Embeddable Widget │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ <refined-ar-viewer model-id="..." /> │ │
│ │ Embed in any website to enable AR viewing │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
| Category | Version |
|---|---|
| Runtime | Node.js 22.x LTS |
| Backend Framework | NestJS 10.x |
| Frontend Framework | Next.js 15.x |
| Web Components | Stencil.js 4.x |
| Database | PostgreSQL 14 |
| Cache/Queue | Redis 7 |
| ORM | TypeORM 0.3.x |
| Authentication | JWT + Passport |
| File Storage | AWS S3 |
| Containerization | Docker |
- Docker and Docker Compose v2.x (recommended for running the full stack)
- Node.js 22.x LTS (for local development without Docker)
- Git
- AWS account with S3 access (for file storage)
git clone https://github.com/nomtek/refined-ar.git
cd refined-arThe repository structure:
refined-ar/
├── docker-compose.yml
├── refined-ar-backend/
├── refined-ar-next-js/
├── refined-ar-converter-api/
├── refined-ar-converter-app/
├── refined-commons/
└── web-component/
cd refined-commons
npm install
npm run build
cd ..Create refined-ar-backend/.docker-compose.env:
POSTGRES_USER=postgres
POSTGRES_PASSWORD=your_secure_password_here
POSTGRES_DB=refined_ar_devCreate refined-ar-backend/.env:
# Database
TYPEORM_CONNECTION=postgres
TYPEORM_HOST=postgres
TYPEORM_USERNAME=postgres
TYPEORM_PASSWORD=your_secure_password_here
TYPEORM_DATABASE=refined_ar_dev
TYPEORM_PORT=5432
TYPEORM_SYNCHRONIZE=false
TYPEORM_LOGGING=true
TYPEORM_MIGRATIONS_DIR=dist/src/migrations
TYPEORM_MIGRATIONS=dist/src/migrations/*.js
# Authentication
JWT_SECRET=your-jwt-secret-key-minimum-32-chars
# URLs
BASE_URL=http://localhost:4000
FRONTEND_HOST=http://localhost:3000
CORS_WHITELIST=http://localhost:3000,http://localhost:3001
PORT=4000
# AWS S3 (Required)
DEFAULT_S3_BUCKET_NAME=your-bucket-name
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
# Redis
REDIS_URL=redis://redis:6379
# Token Settings
ACCESS_TOKEN_EXPIRATION=12h
REFRESH_TOKEN_EXPIRATION=30d
# Product Limits
PREMIUM_USER_MAX_PRODUCTS=30
DEFAULT_USER_MAX_PRODUCTS=15Create refined-ar-converter-api/.env:
# AWS Configuration
AWS_BUCKET_NAME=your-bucket-name
AWS_REGION=eu-central-1
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
# API Key (Required - generate a secure random string)
CONVERTER_API_KEY=your-secure-api-key-here-32chars
# CORS Configuration
CORS_WHITELIST=http://localhost:3000,http://localhost:3001
# Environment
NODE_ENV=development
PORT=5000Create refined-ar-next-js/.env.local:
NEXT_PUBLIC_API_URL=http://localhost:4000
NEXT_PUBLIC_CONVERTER_API_URL=http://localhost:5001
NEXT_PUBLIC_CONVERTER_API_KEY=your-secure-api-key-here-32charsCreate refined-ar-converter-app/.env.local:
NEXT_PUBLIC_API_URL=http://localhost:5001docker compose build
docker compose up| Service | URL | Notes |
|---|---|---|
| Main Dashboard | http://localhost:3000 | Login/register page |
| Backend API | http://localhost:4000 | REST API |
| Swagger Docs | http://localhost:4000/api | API documentation |
| Converter API | http://localhost:5001 | Requires X-API-Key header |
| Converter App | http://localhost:3001 | Standalone converter UI |
The refined-ar-viewer web component allows you to embed AR viewing capability in any website.
See the included index.html for a working example:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RefinedAR Demo</title>
<!-- Load the RefinedAR widget -->
<script type="module" src="https://your-domain.com/widget/refinedar.esm.js"></script>
</head>
<body>
<refined-ar-viewer
model-id="your-product-uuid"
lang="en"
/>
</body>
</html>Step 1: Add the embedding page's origin to the backend CORS whitelist
The widget makes API calls to the backend, so the page embedding it must be in the backend's CORS_WHITELIST. For example, if embedding on https://my-shop.com:
CORS_WHITELIST=https://your-domain.com,https://my-shop.comStep 2: Add the script tag
<script type="module" src="https://your-domain.com/widget/refinedar.esm.js"></script>Step 3: Add the component
<refined-ar-viewer
model-id="6c1da829-20b2-4cd8-8bc4-f4eba1c69173"
lang="en"
button-style="black"
/>| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
modelId |
model-id |
string |
— | Required. Product UUID from the backend API |
lang |
lang |
string |
"en" |
Language code (en, de, fr, etc.) |
buttonStyle |
button-style |
string |
"black" |
"black", "white", or "custom" |
buttonTitle |
button-title |
string |
"View in AR" |
Custom button text |
modalTitle |
modal-title |
string |
Auto | QR modal title |
modalDescription |
modal-description |
string |
Auto | QR modal description |
modalButtonTitle |
modal-button-title |
string |
Auto | QR modal close button text |
gaTracker |
ga-tracker |
string |
— | Google Analytics (Universal) tracking ID |
gtmTracker |
gtm-tracker |
string |
— | Google Tag Manager container ID |
<refined-ar-viewer
lang="en"
model-id="6c1da829-20b2-4cd8-8bc4-f4eba1c69173"
button-style="white"
button-title="View in AR"
modal-title="Scan to View in AR"
modal-description="Scan this QR code with your mobile device"
modal-button-title="Close"
ga-tracker="UA-XXXXXXXXX-X">
</refined-ar-viewer><refined-ar-viewer model-id="your-model-id" lang="en"></refined-ar-viewer>
<script>
const viewer = document.querySelector('refined-ar-viewer');
viewer.addEventListener('buttonClicked', (event) => {
console.log('Button clicked:', event.detail.type); // 'AR' or 'QR'
});
</script>- Via Dashboard (http://localhost:3000): Upload a product with a 3D model
- Via Converter App (http://localhost:3001): Upload and convert a GLTF/GLB file
- Via API: POST to
/api/converter/convertwithX-API-Keyheader
When self-hosting, update the widget configuration in web-component/config/globalScript.prod.ts:
export const config: EnvironmentConfig = {
api: 'https://api.your-domain.com/product/',
refinedLogoUrl: 'https://your-domain.com/images/refined-logo.svg',
refinedQrLogo: 'https://your-domain.com/images/qr-refined-logo.svg',
refinedLandingUrl: 'https://your-domain.com',
refinedViewUrl: 'https://your-domain.com/view/',
};Then build the widget:
cd web-component
npm install
npm run buildProdThe compiled files will be in web-component/dist/refinedar/.
Deploy the full stack in under 5 minutes using pre-built images from GitHub Container Registry.
| Image | Description |
|---|---|
ghcr.io/nomtek/refined-ar-backend |
Core API server (NestJS) |
ghcr.io/nomtek/refined-ar-converter-api |
3D model conversion microservice |
ghcr.io/nomtek/refined-ar-dashboard |
Main dashboard (Next.js) |
ghcr.io/nomtek/refined-ar-converter-app |
Converter web UI (Next.js) |
Every push to the default branch publishes images with two tags:
| Tag | Example | Description |
|---|---|---|
latest |
ghcr.io/nomtek/refined-ar-backend:latest |
Always points to the newest build |
<commit-sha> |
ghcr.io/nomtek/refined-ar-backend:<sha> |
Pinned to a specific commit |
Use :latest for the simplest setup — it always pulls the newest version. To pin a specific version, find the commit SHA from GitHub Packages and use it as the tag instead.
# 1. Get the deployment files
curl -O https://raw.githubusercontent.com/nomtek/refined-ar/main/docker-compose.ghcr.yml
curl -O https://raw.githubusercontent.com/nomtek/refined-ar/main/.env.production.example
# 2. Configure
cp .env.production.example .env
# Edit .env with your values (see Environment Variables Reference below)
# 3. Deploy
docker compose -f docker-compose.ghcr.yml --env-file .env up -d
# 4. Run database migrations
docker exec $(docker ps -qf "name=backend") npx typeorm migration:run --dataSource ./dist/src/data-source.js
# 5. Open http://localhost:3000All NEXT_PUBLIC_* variables are injected at container startup — no rebuilding needed.
To pin a specific version, replace :latest with the commit SHA tag in docker-compose.ghcr.yml for each service. You can find available tags in GitHub Packages.
RefinedAR works with any S3-compatible storage. Add these to your .env:
S3_ENDPOINT_URL=https://your-s3-endpoint.com # e.g. http://minio:9000
S3_FORCE_PATH_STYLE=true # required for most S3-compatible providersLeave both empty/unset for standard AWS S3.
See .env.production.example for the full list with comments. The essential ones:
| Variable | Description |
|---|---|
POSTGRES_PASSWORD |
Database password |
JWT_SECRET |
Auth signing secret (64+ chars) |
BASE_URL |
Public backend URL (e.g. https://api.your-domain.com) |
FRONTEND_HOST |
Public dashboard URL (e.g. https://your-domain.com) |
CORS_WHITELIST |
Comma-separated allowed origins |
AWS_ACCESS_KEY_ID |
S3 credentials |
AWS_SECRET_ACCESS_KEY |
S3 credentials |
AWS_BUCKET_NAME |
S3 bucket name |
CONVERTER_API_KEY |
Converter API auth key (32+ chars) |
NEXT_PUBLIC_API_URL |
Backend URL (as seen by browsers) |
NEXT_PUBLIC_CONVERTER_API_URL |
Converter API URL (as seen by browsers) |
If you want to customize the images or use your own registry:
# Clone the repo
git clone https://github.com/nomtek/refined-ar.git
cd refined-ar
# Build all images
docker compose build
# Or build individually
docker build -f refined-ar-backend/Dockerfile -t my-registry/refined-ar-backend:latest refined-ar-backend
docker build -f refined-ar-next-js/refined-ar-next-js/Dockerfile -t my-registry/refined-ar-dashboard:latest .Coolify is a self-hosted PaaS that simplifies deployment.
- Create a Docker Compose resource in Coolify
- Paste contents of
docker-compose.ghcr.yml - Add environment variables from
.env.production.example - Deploy
After deploying, the AR viewer widget is served from the dashboard at:
https://your-domain.com/widget/refinedar.esm.js
Embed it in any website:
<script type="module" src="https://your-domain.com/widget/refinedar.esm.js"></script>
<refined-ar-viewer model-id="<product-uuid>" lang="en" />cd refined-ar-backend
# Start PostgreSQL and Redis via Docker
docker compose up postgres redis -d
# Install and run
npm install
npm run build
npm run start:devcd refined-ar-next-js
npm install
npm run devcd refined-ar-converter-api
npm install
npm run build
npm run start:devNote: USDZ conversion requires the
usd_from_gltfbinary. The Docker image includes this, but for local development you may need to install it separately or conversions will only produce GLB files.
cd refined-ar-converter-app
npm install
npm run devcd web-component
npm install
npm run start # Dev server with hot reload
# or
npm run buildDev # Build for development (points to localhost:4000)
npm run buildProd # Build for production# Via Docker
docker exec -it refined-ar-db psql -U postgres -d refined_ar_dev
# Create test database
docker exec -it refined-ar-db psql -U postgres -c "CREATE DATABASE refined_ar_test;"Migrations run automatically on backend startup. To run manually:
docker exec -it refined-ar-backend bash
npm run build
npx typeorm migration:run --dataSource ./dist/src/data-source.js| Variable | Required | Default | Description |
|---|---|---|---|
POSTGRES_USER |
No | postgres |
Database user |
POSTGRES_PASSWORD |
Yes | — | Database password |
POSTGRES_DB |
No | refined_ar |
Database name |
| Variable | Required | Default | Description |
|---|---|---|---|
TYPEORM_CONNECTION |
No | postgres |
Database type |
TYPEORM_HOST |
Yes | — | Database host (use postgres in Docker) |
TYPEORM_USERNAME |
Yes | — | Database username |
TYPEORM_PASSWORD |
Yes | — | Database password |
TYPEORM_DATABASE |
Yes | — | Database name |
TYPEORM_PORT |
No | 5432 |
Database port |
TYPEORM_SYNCHRONIZE |
No | false |
Auto-sync schema (keep false in production) |
TYPEORM_LOGGING |
No | true |
Enable query logging |
TYPEORM_MIGRATIONS_DIR |
No | dist/src/migrations |
Migrations directory |
TYPEORM_MIGRATIONS |
No | dist/src/migrations/*.js |
Migration files pattern |
DB_SSL_CA_PATH |
No | — | Path to SSL CA certificate for database |
| Variable | Required | Default | Description |
|---|---|---|---|
JWT_SECRET |
Yes | — | Secret for JWT signing (64+ chars recommended) |
ACCESS_TOKEN_EXPIRATION |
No | 12h |
JWT access token expiry |
REFRESH_TOKEN_EXPIRATION |
No | 30d |
JWT refresh token expiry |
COOKIE_DOMAIN |
No | — | Cookie domain for cross-subdomain auth (e.g. .your-domain.com) |
| Variable | Required | Default | Description |
|---|---|---|---|
BASE_URL |
Yes | http://localhost:4000 |
Public backend URL |
FRONTEND_HOST |
Yes | — | Public frontend URL (used in emails, redirects) |
CORS_WHITELIST |
Yes | — | Comma-separated allowed origins |
PORT |
No | 4000 |
HTTP port |
| Variable | Required | Default | Description |
|---|---|---|---|
AWS_ACCESS_KEY_ID |
Yes | — | S3 access key |
AWS_SECRET_ACCESS_KEY |
Yes | — | S3 secret key |
DEFAULT_S3_BUCKET_NAME |
Yes | — | S3 bucket for file storage |
AWS_REGION |
No | eu-west-2 |
AWS region |
S3_ENDPOINT_URL |
No | — | Custom S3 endpoint (for MinIO, R2, DigitalOcean Spaces) |
S3_FORCE_PATH_STYLE |
No | false |
Set true for most S3-compatible providers |
| Variable | Required | Default | Description |
|---|---|---|---|
REDIS_HOST |
No | redis |
Redis host |
REDIS_PORT |
No | 6379 |
Redis port |
REDIS_PASSWORD |
No | — | Redis password (if authentication enabled) |
| Variable | Required | Default | Description |
|---|---|---|---|
PREMIUM_USER_MAX_PRODUCTS |
No | 30 |
Max products for premium users |
DEFAULT_USER_MAX_PRODUCTS |
No | 15 |
Max products for regular users |
MODEL_MAX_FILE_SIZE |
No | 104857600 |
Max file upload size in bytes (100 MB) |
| Variable | Required | Default | Description |
|---|---|---|---|
MAILGUN_API_KEY |
No | — | Mailgun API key |
MAILGUN_DOMAIN |
No | — | Mailgun sending domain |
MAILGUN_BASE_URL |
No | https://api.eu.mailgun.net |
Mailgun API base URL |
EMAIL_SENDER_USERNAME |
No | no-reply@example.com |
Sender email address |
| Variable | Required | Default | Description |
|---|---|---|---|
SLACK_WEBHOOK_URL |
No | — | Slack incoming webhook URL |
SLACK_WEBHOOK_ENABLED |
No | false |
Enable Slack notifications |
| Variable | Required | Default | Description |
|---|---|---|---|
STRIPE_SECRET_KEY |
No | — | Stripe secret key for payment processing |
| Variable | Required | Default | Description |
|---|---|---|---|
AWS_BUCKET_NAME |
Yes | — | S3 bucket for converted files |
AWS_ACCESS_KEY_ID |
Yes | — | S3 access key |
AWS_SECRET_ACCESS_KEY |
Yes | — | S3 secret key |
AWS_REGION |
No | eu-central-1 |
AWS region |
CONVERTER_API_KEY |
Yes | — | API key for authentication (32+ chars) |
CORS_WHITELIST |
Yes | — | Comma-separated allowed origins |
S3_ENDPOINT_URL |
No | — | Custom S3 endpoint (for MinIO, R2, etc.) |
S3_FORCE_PATH_STYLE |
No | false |
Set true for S3-compatible providers |
PORT |
No | 5000 |
HTTP port |
NODE_ENV |
No | development |
Environment (development or production) |
All NEXT_PUBLIC_* variables are injected at container startup via docker-entrypoint.sh — no rebuild needed.
| Variable | Required | Default | Description |
|---|---|---|---|
NEXT_PUBLIC_API_URL |
Yes | http://localhost:4000 |
Backend API URL (as seen by browsers) |
NEXT_PUBLIC_CONVERTER_API_URL |
Yes | http://localhost:5000 |
Converter API URL (as seen by browsers) |
NEXT_PUBLIC_CONVERTER_API_KEY |
Yes | — | Converter API key (must match CONVERTER_API_KEY) |
| Variable | Required | Default | Description |
|---|---|---|---|
NEXT_PUBLIC_APP_NAME |
No | MyApp |
Application name shown in UI |
NEXT_PUBLIC_WEBSITE_URL |
No | https://your-website.com |
Company website URL |
NEXT_PUBLIC_CONTACT_EMAIL |
No | — | Contact email address |
NEXT_PUBLIC_MANUAL_URL |
No | /guide |
Help/documentation URL |
NEXT_PUBLIC_PRIVACY_POLICY_URL |
No | — | Privacy policy URL |
NEXT_PUBLIC_PRODUCTS_COUNT_LIMIT |
No | 1 |
Product limit display |
| Variable | Required | Default | Description |
|---|---|---|---|
NEXT_PUBLIC_GOOGLE_ANALYTICS |
No | — | Google Analytics tracking ID |
NEXT_PUBLIC_HOTJAR |
No | — | Hotjar analytics tracking ID |
NEXT_PUBLIC_AIRBRAKE_PROJECT_ID |
No | — | Airbrake error tracking project ID |
NEXT_PUBLIC_AIRBRAKE_PROJECT_KEY |
No | — | Airbrake error tracking project key |
All NEXT_PUBLIC_* variables are injected at container startup via docker-entrypoint.sh — no rebuild needed.
| Variable | Required | Default | Description |
|---|---|---|---|
NEXT_PUBLIC_API_URL |
Yes | http://localhost:5001/api/converter/convert |
Converter API endpoint |
NEXT_PUBLIC_CONVERTER_API_KEY |
Yes | — | API authentication key (must match CONVERTER_API_KEY) |
NEXT_PUBLIC_APP_NAME |
No | converter.AR |
Application name |
NEXT_PUBLIC_WEBSITE_URL |
No | https://www.refined.ar/ |
Branding website URL |
NEXT_PUBLIC_WIDGET_URL |
No | — | Embedded widget JavaScript URL |
NEXT_PUBLIC_SITE_URL |
No | — | Website URL for SEO meta tags |
NEXT_PUBLIC_OG_IMAGE_URL |
No | — | Open Graph image URL |
NEXT_PUBLIC_CONTACT_EMAIL |
No | — | Contact email address |
NEXT_PUBLIC_FOOTER_TEXT |
No | — | Custom footer text |
NEXT_PUBLIC_MANUAL_URL |
No | /guide |
Help guide URL |
NEXT_PUBLIC_SOCIAL_LINKEDIN |
No | — | LinkedIn profile URL |
NEXT_PUBLIC_SOCIAL_TWITTER |
No | — | Twitter profile URL |
NEXT_PUBLIC_SOCIAL_FACEBOOK |
No | — | Facebook profile URL |
NEXT_PUBLIC_GOOGLE_ANALYTICS |
No | — | Google Analytics tracking ID |
NEXT_PUBLIC_HOTJAR |
No | — | Hotjar analytics tracking ID |
NEXT_PUBLIC_AIRBRAKE_PROJECT_ID |
No | — | Airbrake error tracking project ID |
NEXT_PUBLIC_AIRBRAKE_PROJECT_KEY |
No | — | Airbrake error tracking project key |
| Service | Container Port | Host Port |
|---|---|---|
| PostgreSQL | 5432 | 54321 |
| Redis | 6379 | 6379 |
| Backend API | 4000 | 4000 |
| Converter API | 5000 | 5001 |
| Dashboard | 3000 | 3000 |
| Converter App | 3000 | 3001 |
- Minimum 12 characters
- At least one uppercase letter (A-Z)
- At least one lowercase letter (a-z)
- At least one digit (0-9)
- At least one special character (
@$!%*?&)
- Converter API requires
X-API-Keyheader on all requests - Use a secure random string of 32+ characters for
CONVERTER_API_KEY - Never expose API keys in client-side code (use environment variables)
- Never commit
.envfiles with real credentials - Use
.env.examplefiles as templates - Rotate secrets regularly in production
- Consider using a secrets manager (AWS Secrets Manager, HashiCorp Vault)
docker compose down
docker system prune -f
docker compose build --no-cache
docker compose uplsof -i :4000
kill -9 <PID>docker exec -it refined-ar-db psql -U postgres -c "SELECT 1"
docker compose logs postgresEnsure:
CONVERTER_API_KEYis set in.env- Requests include
X-API-Key: <your-key>header
- Check browser console for CORS errors
- Verify the widget script URL is correct
- Ensure
model-idis a valid product UUID from your backend - Check that the backend API is accessible from the browser
Pull requests are welcome! For significant changes, please open an issue first.
- Code style enforced with ESLint and Prettier — run
npm run lint - All existing tests must pass
- Add tests for new functionality
- GitHub Repository
- Issue Tracker
- Docker Images (GHCR) — pre-built images for all services