Turn a single photo into a downloadable, browser-viewable 3D asset.
Sign in, upload a picture, and an async pipeline handles screening, preprocessing, AI reconstruction, mesh cleanup, and storage, no 3D modeling experience required.
3dify is a web service that turns a single photo of an object into a
downloadable GLB (glTF binary) 3D asset. A user signs in with Google,
uploads a photo, and a background pipeline runs it through content
moderation, preprocessing, AI-based 3D reconstruction, and mesh
repair, landing the finished, watertight mesh in a permanent asset
library with an in-browser <model-viewer> preview and a download link.
Reconstruction is served by TripoSR running as a serverless GPU function on Modal (scale-to-zero, no idle GPU cost), with a higher-fidelity TRELLIS engine available as a drop-in upgrade behind the same interface.
Key properties:
- Asynchronous, resumable pipeline: BullMQ-backed job queue with a pure,
fully-tested state machine driving
queued → moderating → preprocessing → reconstructing → postprocessing → exporting → succeeded - Pluggable reconstruction engine: swap TripoSR, TRELLIS, or a local stub cube via a single env var, no code changes
- Production-shaped storage: Cloudinary for source images, any S3-compatible bucket (MinIO locally, Backblaze B2/Cloudflare R2 in prod) for generated assets
- Guardrails baked in: upload format/size/dimension enforcement, optional Cloudinary moderation gate, terminal vs. transient failure handling with retries
- Node.js 22+ and pnpm
- Docker (Postgres, Redis, MinIO run in containers)
- A Google OAuth client
- A Cloudinary account (free tier is fine)
- Optional: a Modal account, for real (non-stub) 3D reconstruction
git clone https://github.com/DevloperHS/3dify_oss.git
cd 3dify_oss
pnpm installcp .env.example .envFollow the inline comments in .env.example for exactly where each value
comes from; the table below is just the minimum you need and where to sign
up for each service:
| Variable | Purpose | Sign up |
|---|---|---|
BETTER_AUTH_SECRET |
Session signing, generate with npx @better-auth/cli secret |
n/a |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
Sign-in (redirect URI: http://localhost:3000/api/auth/callback/google) |
console.cloud.google.com/apis/credentials |
CLOUDINARY_CLOUD_NAME / CLOUDINARY_API_KEY / CLOUDINARY_API_SECRET |
Source image storage | cloudinary.com |
S3_* |
Generated GLB storage; MinIO defaults already work out of the box | n/a (or backblaze.com/b2 for prod) |
RECONSTRUCTION_ENGINE, MODAL_* |
stub (no account needed), modal (TripoSR), or trellis |
modal.com |
docker compose up -d # Postgres, Redis, MinIO (+ bucket bootstrap)
pnpm db:push # sync the Drizzle schemapnpm dev # Next.js on http://localhost:3000
pnpm worker # BullMQ worker, run in a second terminalSign in, upload a photo, and watch it progress through the pipeline on the
job page. With RECONSTRUCTION_ENGINE=stub you'll get a placeholder cube;
switch to modal (after modal deploy modal/triposr_app.py) for a real mesh.
pnpm typecheck # tsc --noEmit
pnpm test # vitest, needs docker compose up (hits a real test DB)
pnpm build # production buildflowchart LR
U([User]) -->|upload photo| API[Next.js API route]
API -->|store image| CLD[(Cloudinary)]
API -->|insert Job row| PG[(Postgres)]
API -->|enqueue jobId| Q[(Redis / BullMQ)]
API -.->|201, jobId| U
Q --> W[BullMQ Worker]
W --> MOD[Moderation gate]
MOD --> PRE[Preprocessing<br/>downscale, validate]
PRE --> RECON[Reconstruction Engine]
RECON -->|HTTP| MODAL[TripoSR / TRELLIS<br/>on Modal GPU]
MODAL -->|GLB mesh| RECON
RECON --> POST[Postprocessing<br/>watertight repair]
POST --> EXP[Export to storage]
EXP -->|GLB bytes| S3[(S3 / MinIO)]
EXP -->|insert Asset row| PG
U -->|poll every 2.5s| API
API --> PG
U -->|view / download| S3
The job status itself is driven by a pure, dependency-free state machine, the most heavily tested module in the codebase, since it encodes every business rule about how a job may progress:
stateDiagram-v2
[*] --> queued
queued --> moderating
moderating --> preprocessing
preprocessing --> reconstructing
reconstructing --> postprocessing
postprocessing --> exporting
exporting --> succeeded
succeeded --> [*]
queued --> failed
moderating --> failed
preprocessing --> failed
reconstructing --> failed
postprocessing --> failed
exporting --> failed
failed --> [*]
Rules encoded: transitions are strictly forward-only, unbuilt/optional
stages may be skipped, and terminal states (succeeded, failed) have no
exits. failed is reachable from any non-terminal state so a crash at any
stage surfaces a clean, retryable failure rather than a stuck job.
| Layer | Technology |
|---|---|
| Framework | |
| Language | |
| Styling | |
| Auth | |
| Database | |
| Queue | |
| 3D Reconstruction | |
| Image Storage | |
| Asset Storage | |
| 3D Viewer | |
| Testing | |
| Infra |
Contributions are welcome: bug fixes, new reconstruction engines, or pipeline stage improvements.
- Fork the repo and create a feature branch off
main:git checkout -b feat/short-description
- Set up locally: follow How to Setup above.
RECONSTRUCTION_ENGINE=stubis enough for pipeline/UI work that doesn't touch reconstruction quality itself. - Make your change, keeping it scoped: one logical change per PR.
- Verify before pushing:
pnpm typecheck pnpm test pnpm build - Commit with a clear, imperative message (e.g.
Fix watertight repair on meshes with disjoint boundary loops), explaining why when it isn't obvious from the diff. - Open a pull request against
maindescribing what changed and how you verified it (tests added/updated, manual repro steps, etc.).
For larger changes (a new reconstruction engine, a new pipeline stage), open an issue first to discuss approach before investing implementation time.
MIT © 2026 DevloperHS