atproto Thingiverse but good
10

Configure Feed

Select the types of activity you want to include in your feed.

rewrote readme, reformatted lexicons

Orual (Jun 29, 2026, 7:06 PM EDT) bb3858da b584b329

+163 -41
+38 -12
README.md
··· 1 1 # Polymodel 2 2 3 - Polymodel is an AT Protocol-native model/project sharing app: roughly “Thingiverse on atproto.” 3 + Polymodel (currently hosted intermittently at [polymodel.space](https://polymodel.space)) is an atproto-native 3D model sharing app: roughly “Thingiverse/Printables on atproto, but good.” 4 + 5 + It's currently somewhere between "proof of concept" and "functional alpha". The core things work, including very basic social features, but there is a lot left to build and a lot of usability improvements needed and bugs to fix. 6 + 7 + You can upload and share STL, STEP, OBJ, and 3MF files, as well as a couple of other incidental formats (GLTF, GLB), download them, preview them, add images, instructional text, tag them, and so on. You can follow other users, like or save their creations. 8 + 9 + ## Lexicons and data model 10 + 11 + ALL lexicons currently in use should be expected to evolve in breaking ways in the near future, as I refine the desired semantics. They remain unpublished for this reason. 12 + 13 + #### Core thing 14 + 15 + A 'part' is a file with metadata; a name, dimensions, units, notes, format, preview images, attribution links, and so on. A 'model' is made up of multiple parts, and can represent a thing in itself, or something like a variant of something. A 'thing' is sort of the major project grouping, and includes more detailed text fields, instructions, multiple models, and so on. So for example, *Gridfinity* might be a 'thing', with all the different parts the maker designed as part of the system being 'parts', arranged into 'models'. 4 16 5 - The initial concept is intentionally small: 17 + Currently 'thing' is the only real grouping exposed top-level, but this isn't the long-term intent. Loose 'parts' and 'models' and so on are intended to be surfacable, at least for the user that uploaded them. 6 18 7 - - a lexicon for printable/model projects; 8 - - a profile-oriented publishing surface; 9 - - likes/saves and basic social feedback; 10 - - feeds for recent projects, hot projects, and per-profile project streams; 11 - - an in-browser STL viewer. 19 + #### Collaboration and Remixing 12 20 13 - It is also a testbed for a highly integrated agent workflow built around Polytoken, Jira, Confluence, jj workspaces, and strong review gates. 21 + The current 'thing'-related types have a couple of reference fields, one for "derived from" relations, and the other for "indented to work with" relations. These are pretty preliminary and I'm still working out the semantics I want here. One goal of mine is to make it so that 'derived from' attribution in the interface is more or less automatic, even if the uploader didn't explcitly remix, likely via some form of perceptual hashing. I don't intend to include more than minimal in-browser editing. 22 + 23 + Things like 'makes' or 'printer/slicer configs' and gcode are intended to be added as well. 'part' has a very preliminary take on print settings which *will* change, and the lexicons for "hey i made this `<link>`" straight up don't exist yet. 24 + 25 + #### The rest 26 + 27 + There are tags (currently exposed in the publish UI only, self-applied), intended to be user-supplied and/or community-applied. These are intended as curation and discoverability tools, not for moderation. Polymodel will have something more like moderation labels as well. Models and things have mandatory fields for license, so that copyright and use of original work is very clear. "All rights reserved" models or things will optionally be ineligible for remixing. The default is currently CC-BY-4.0. 28 + 29 + There are lists (not currently exposed in the UI). There is the ability to save a model, bookmark it. Currently this is public, it will be made private. There are 'likes' and profiles with follows (currently using unidirectional follow semantics, this will likely evolve to an optional bidirectional request/accept pattern). 30 + 31 + It has basic search and simple aggregated global and author feeds. 32 + 33 + > Larger text fields will in future support some subset of Weaver-style Markdown, once I port Weaver to Jacquard 0.12.X and fix some more bugs in its editor. I also intend to support other types of 3D content not intended for printing, including models for use in animation or video game production, or things like gaussian splats and point clouds. 34 + 35 + ## Architecture and Evolution 36 + 37 + The current design is a monolithic Dioxus fullstck webapp+appview server, acting as an OAuth (potentially confidential) client. It backfills and tails the firehose via [hydrant](https://hydrant.klbr.net/) into its Fjall data store. That event stream is teed and written to an SQLite database for additional indexing, which is currently used as the primary read target and to support immediate readback of user writes. This is very likely to change, perhaps by sharding the SQLite database by DID and/or moving more into the Fjall store. 38 + 39 + It also intends to support the draft spec for atproto permissioned data, for features such as drafts, private sharing, private lists, and so on. The core primitives will land upstream in Jacquard, and the concrete implementation will be a separate service binary from the main app server, ideally somewhat agnostic to the specifics of the app, acting as space host and permission broker. 14 40 15 41 16 42 ## Development ··· 19 45 20 46 ```sh 21 47 nix develop 48 + # or 49 + direnv allow 22 50 ``` 23 51 24 52 Common commands: ··· 31 59 just serve # Dioxus dev server 32 60 ``` 33 61 34 - `just serve` starts against the current projection database without seeding demo 62 + `just serve` starts against the current databases without seeding demo 35 63 content. For browser e2e or a local demo feed, opt in explicitly: 36 64 `POLYMODEL_SAMPLE_DATA=auto just serve`. 37 65 38 66 ## Stack 39 67 40 - - Rust 2024 / nightly 41 68 - Dioxus 0.7 42 69 - Jacquard 0.12 43 - - Nix flake dev shell 44 - - just command runner 70 + - Hydrant for backfill (used as a library)
+6 -5
lexicons/actor/defs.json
··· 11 11 "handle": { "type": "string" }, 12 12 "displayName": { "type": "string" }, 13 13 "description": { "type": "string" }, 14 - "avatar": { "type": "string", "format": "uri", "description": "Resolved avatar URL (CDN). Null until the app view resolves the polymodel profile avatar blob." }, 14 + "avatar": { 15 + "type": "string", 16 + "format": "uri", 17 + "description": "Resolved avatar URL (CDN). Null until the app view resolves the polymodel profile avatar blob." 18 + }, 15 19 "record": { "type": "unknown", "description": "The raw space.polymodel.actor.profile record." }, 16 20 "defaultLicense": { "type": "string" }, 17 21 "pronouns": { "type": "string" }, ··· 43 47 "profileViewUnion": { 44 48 "type": "union", 45 49 "description": "Discriminates a polymodel-native actor (space.polymodel.actor.defs#profileView, hydrated from the local profile record + stats) from a bluesky-only actor (app.bsky.actor.defs#profileViewDetailed, fetched from the public AppView). Clients render based on $type.", 46 - "refs": [ 47 - "space.polymodel.actor.defs#profileView", 48 - "app.bsky.actor.defs#profileViewDetailed" 49 - ], 50 + "refs": ["space.polymodel.actor.defs#profileView", "app.bsky.actor.defs#profileViewDetailed"], 50 51 "closed": true 51 52 } 52 53 }
+5 -1
lexicons/actor/profile.json
··· 14 14 "avatar": { "type": "blob", "accept": ["image/png", "image/jpeg"], "maxSize": 1048576 }, 15 15 "defaultLicense": { "type": "string" }, 16 16 "pronouns": { "type": "string", "maxGraphemes": 1024, "maxLength": 10240 }, 17 - "printers": { "type": "array", "items": { "type": "string", "maxGraphemes": 1024, "maxLength": 10240 }, "maxLength": 32 }, 17 + "printers": { 18 + "type": "array", 19 + "items": { "type": "string", "maxGraphemes": 1024, "maxLength": 10240 }, 20 + "maxLength": 32 21 + }, 18 22 "links": { "type": "array", "items": { "type": "string", "format": "uri" }, "maxLength": 16 } 19 23 } 20 24 }
+4 -1
lexicons/graph/getList.json
··· 19 19 "schema": { 20 20 "type": "object", 21 21 "properties": { 22 - "items": { "type": "array", "items": { "type": "ref", "ref": "space.polymodel.library.defs#thingViewBasic" } }, 22 + "items": { 23 + "type": "array", 24 + "items": { "type": "ref", "ref": "space.polymodel.library.defs#thingViewBasic" } 25 + }, 23 26 "cursor": { "type": "string" } 24 27 }, 25 28 "required": ["items"]
+25 -5
lexicons/library/model.json
··· 13 13 "summary": { "type": "string", "maxGraphemes": 300, "maxLength": 2000 }, 14 14 "instructions": { "type": "array", "items": { "type": "string" }, "maxLength": 100 }, 15 15 "license": { "type": "string" }, 16 - "tags": { "type": "array", "items": { "type": "string", "maxGraphemes": 32, "maxLength": 64 }, "maxLength": 16 }, 17 - "cover": { "type": "array", "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, "maxLength": 8 }, 18 - "previews": { "type": "array", "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, "maxLength": 12 }, 19 - "parts": { "type": "array", "items": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, "maxLength": 1024 }, 16 + "tags": { 17 + "type": "array", 18 + "items": { "type": "string", "maxGraphemes": 32, "maxLength": 64 }, 19 + "maxLength": 16 20 + }, 21 + "cover": { 22 + "type": "array", 23 + "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, 24 + "maxLength": 8 25 + }, 26 + "previews": { 27 + "type": "array", 28 + "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, 29 + "maxLength": 12 30 + }, 31 + "parts": { 32 + "type": "array", 33 + "items": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, 34 + "maxLength": 1024 35 + }, 20 36 "derivedFrom": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, 21 - "intendedToWorkWith": { "type": "array", "items": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, "maxLength": 32 }, 37 + "intendedToWorkWith": { 38 + "type": "array", 39 + "items": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, 40 + "maxLength": 32 41 + }, 22 42 "createdAt": { "type": "string", "format": "datetime" } 23 43 }, 24 44 "required": ["name", "parts", "createdAt"]
+10 -2
lexicons/library/part.json
··· 16 16 "units": { "type": "string", "default": "mm" }, 17 17 "notes": { "type": "string", "maxGraphemes": 300, "maxLength": 2000 }, 18 18 "printSettings": { "type": "array", "items": { "type": "string" }, "maxLength": 50 }, 19 - "previews": { "type": "array", "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, "maxLength": 12 }, 19 + "previews": { 20 + "type": "array", 21 + "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, 22 + "maxLength": 12 23 + }, 20 24 "derivedFrom": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, 21 - "intendedToWorkWith": { "type": "array", "items": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, "maxLength": 32 }, 25 + "intendedToWorkWith": { 26 + "type": "array", 27 + "items": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, 28 + "maxLength": 32 29 + }, 22 30 "createdAt": { "type": "string", "format": "datetime" } 23 31 }, 24 32 "required": ["name", "file", "createdAt"]
+50 -10
lexicons/library/publishThing.json
··· 39 39 "units": { "type": "string" }, 40 40 "notes": { "type": "string", "maxGraphemes": 300, "maxLength": 2000 }, 41 41 "printSettings": { "type": "array", "items": { "type": "string" }, "maxLength": 50 }, 42 - "previews": { "type": "array", "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, "maxLength": 12 }, 42 + "previews": { 43 + "type": "array", 44 + "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, 45 + "maxLength": 12 46 + }, 43 47 "derivedFrom": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, 44 - "intendedToWorkWith": { "type": "array", "items": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, "maxLength": 32 } 48 + "intendedToWorkWith": { 49 + "type": "array", 50 + "items": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, 51 + "maxLength": 32 52 + } 45 53 } 46 54 }, 47 55 "modelInput": { ··· 52 60 "summary": { "type": "string", "maxGraphemes": 300, "maxLength": 2000 }, 53 61 "instructions": { "type": "array", "items": { "type": "string" }, "maxLength": 100 }, 54 62 "license": { "type": "string" }, 55 - "tags": { "type": "array", "items": { "type": "string", "maxGraphemes": 32, "maxLength": 64 }, "maxLength": 16 }, 56 - "cover": { "type": "array", "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, "maxLength": 8 }, 57 - "previews": { "type": "array", "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, "maxLength": 12 }, 63 + "tags": { 64 + "type": "array", 65 + "items": { "type": "string", "maxGraphemes": 32, "maxLength": 64 }, 66 + "maxLength": 16 67 + }, 68 + "cover": { 69 + "type": "array", 70 + "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, 71 + "maxLength": 8 72 + }, 73 + "previews": { 74 + "type": "array", 75 + "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, 76 + "maxLength": 12 77 + }, 58 78 "parts": { "type": "array", "items": { "type": "ref", "ref": "#partInput" }, "maxLength": 1024 }, 59 79 "derivedFrom": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, 60 - "intendedToWorkWith": { "type": "array", "items": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, "maxLength": 32 } 80 + "intendedToWorkWith": { 81 + "type": "array", 82 + "items": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, 83 + "maxLength": 32 84 + } 61 85 } 62 86 }, 63 87 "thingInput": { ··· 68 92 "summary": { "type": "string", "maxGraphemes": 300, "maxLength": 2000 }, 69 93 "instructions": { "type": "array", "items": { "type": "string" }, "maxLength": 100 }, 70 94 "license": { "type": "string" }, 71 - "tags": { "type": "array", "items": { "type": "string", "maxGraphemes": 32, "maxLength": 64 }, "maxLength": 16 }, 72 - "cover": { "type": "array", "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, "maxLength": 8 }, 73 - "previews": { "type": "array", "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, "maxLength": 12 }, 95 + "tags": { 96 + "type": "array", 97 + "items": { "type": "string", "maxGraphemes": 32, "maxLength": 64 }, 98 + "maxLength": 16 99 + }, 100 + "cover": { 101 + "type": "array", 102 + "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, 103 + "maxLength": 8 104 + }, 105 + "previews": { 106 + "type": "array", 107 + "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, 108 + "maxLength": 12 109 + }, 74 110 "models": { "type": "array", "items": { "type": "ref", "ref": "#modelInput" }, "maxLength": 256 }, 75 111 "derivedFrom": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, 76 - "intendedToWorkWith": { "type": "array", "items": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, "maxLength": 32 } 112 + "intendedToWorkWith": { 113 + "type": "array", 114 + "items": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, 115 + "maxLength": 32 116 + } 77 117 } 78 118 }, 79 119 "recordRef": {
+25 -5
lexicons/library/thing.json
··· 13 13 "summary": { "type": "string", "maxGraphemes": 300, "maxLength": 2000 }, 14 14 "instructions": { "type": "array", "items": { "type": "string" }, "maxLength": 100 }, 15 15 "license": { "type": "string" }, 16 - "tags": { "type": "array", "items": { "type": "string", "maxGraphemes": 32, "maxLength": 64 }, "maxLength": 16 }, 17 - "cover": { "type": "array", "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, "maxLength": 8 }, 18 - "previews": { "type": "array", "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, "maxLength": 12 }, 19 - "models": { "type": "array", "items": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, "maxLength": 256 }, 16 + "tags": { 17 + "type": "array", 18 + "items": { "type": "string", "maxGraphemes": 32, "maxLength": 64 }, 19 + "maxLength": 16 20 + }, 21 + "cover": { 22 + "type": "array", 23 + "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, 24 + "maxLength": 8 25 + }, 26 + "previews": { 27 + "type": "array", 28 + "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, 29 + "maxLength": 12 30 + }, 31 + "models": { 32 + "type": "array", 33 + "items": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, 34 + "maxLength": 256 35 + }, 20 36 "derivedFrom": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, 21 - "intendedToWorkWith": { "type": "array", "items": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, "maxLength": 32 }, 37 + "intendedToWorkWith": { 38 + "type": "array", 39 + "items": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, 40 + "maxLength": 32 41 + }, 22 42 "createdAt": { "type": "string", "format": "datetime" } 23 43 }, 24 44 "required": ["name", "license", "createdAt"]