[{"data":1,"prerenderedAt":1972},["ShallowReactive",2],{"doc-\u002Fen\u002Fnodes":3,"docnav-en":157},{"id":4,"title":5,"body":6,"description":148,"extension":149,"meta":150,"nav":151,"navigation":152,"path":153,"seo":154,"stem":155,"__hash__":156},"docs\u002Fen\u002Fnodes.md","Edge nodes",{"type":7,"value":8,"toc":139},"minimark",[9,14,18,22,80,83,90,94,97,101,104,108,111,128,132,135],[10,11,13],"h2",{"id":12},"why-an-edge-node","Why an edge node",[15,16,17],"p",{},"Without one, every install at a remote location crosses the link back to your origin. An edge node\nserves the winget API and installer downloads on the local network at that location, and fetches\nfrom the origin only on demand.",[10,19,21],{"id":20},"what-runs-where","What runs where",[23,24,25,38],"table",{},[26,27,28],"thead",{},[29,30,31,35],"tr",{},[32,33,34],"th",{},"Runs on the origin (part of the application you already have)",[32,36,37],{},"Runs at the location (Enterprise edition)",[39,40,41,50,58,66,73],"tbody",{},[29,42,43,47],{},[44,45,46],"td",{},"Enroll and revoke nodes, issue enrollment keys",[44,48,49],{},"The edge node itself — serves the winget API and installer downloads on the local network",[29,51,52,55],{},[44,53,54],{},"Health and heartbeat, node status",[44,56,57],{},"Caches from the origin on first request",[29,59,60,63],{},[44,61,62],{},"Package and tag scope per node",[44,64,65],{},"Serves every request after that locally",[29,67,68,71],{},[44,69,70],{},"Installer filters (architecture, scope)",[44,72],{},[29,74,75,78],{},[44,76,77],{},"Push, pre-stage, evict",[44,79],{},[15,81,82],{},"Node management — enrollment, scope, filters, push\u002Fpre-stage, eviction and health — is part of the\napplication you already run.",[84,85,87],"edition-callout",{"edition":86},"enterprise",[15,88,89],{},"The edge node itself — the part that runs at the location and answers clients there — is part of\nthe Enterprise edition and is deployed separately at each site. It connects back to your origin\nusing an enrollment key you generate in the same screen.",[10,91,93],{"id":92},"enrolling-a-node","Enrolling a node",[15,95,96],{},"Admin → Edge nodes → create one, which issues a one-time enrollment key. Run the node at the\nlocation with that key; it exchanges it for a durable token on first contact, optionally with a\nclient certificate issued by the origin's own CA for mutual TLS on later requests.",[10,98,100],{"id":99},"scope-and-control","Scope and control",[15,102,103],{},"For each node you set which packages or tags it carries, and filter installers by architecture and\nscope, so a small site doesn't cache more than it needs. Caching otherwise happens lazily: a node\nfetches on first request and serves every client after that from its own cache. Pre-staging skips\nthat first slow request — push a version ahead of a rollout so the first client at that site is\nalready served locally. Eviction forces a re-fetch — use it after replacing an installer file at\nthe same URL, or to reclaim space on a node with a tight disk. You can see a node's last-seen\nstatus on the dashboard.",[10,105,107],{"id":106},"on-the-client-side","On the client side",[15,109,110],{},"Clients at that location add the node's own source URL, the same way they'd add the origin's:",[112,113,118],"pre",{"className":114,"code":115,"language":116,"meta":117,"style":117},"language-powershell shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","winget source add --name kvellman --type Microsoft.Rest --arg \"https:\u002F\u002Fnode.example-site.internal\u002Fapi\u002F\u003Csite-token>\"\n","powershell","",[119,120,121],"code",{"__ignoreMap":117},[122,123,126],"span",{"class":124,"line":125},"line",1,[122,127,115],{},[10,129,131],{"id":130},"requirements-at-the-location","Requirements at the location",[15,133,134],{},"A host that can run a container, outbound reachability to the origin, and a trusted HTTPS\ncertificate for the node's own URL — winget's HTTPS requirement applies to a node exactly as it\ndoes to the origin.",[136,137,138],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":117,"searchDepth":140,"depth":140,"links":141},2,[142,143,144,145,146,147],{"id":12,"depth":140,"text":13},{"id":20,"depth":140,"text":21},{"id":92,"depth":140,"text":93},{"id":99,"depth":140,"text":100},{"id":106,"depth":140,"text":107},{"id":130,"depth":140,"text":131},"Serving winget clients at a remote location from a local cache of your origin.","md",{},60,true,"\u002Fen\u002Fnodes",{"title":5,"description":148},"en\u002Fnodes","QcF17l1VA5MjGvH_14yWtRE4jhYvb0xrG4xsyKovBqY",[158,436,603,763,1006,1163,1256,1682,1785],{"id":159,"title":160,"body":161,"description":429,"extension":149,"meta":430,"nav":431,"navigation":152,"path":432,"seo":433,"stem":434,"__hash__":435},"docs\u002Fen\u002Fgetting-started.md","Getting started",{"type":7,"value":162,"toc":420},[163,167,183,187,194,198,205,284,291,295,304,308,311,327,338,342,349,369,372,376,417],[10,164,166],{"id":165},"what-is-kvellman","What is kvellman?",[15,168,169,170,174,175,178,179,182],{},"kvellman is a self-hosted, ",[171,172,173],"strong",{},"winget-compatible"," package repository. You register it as an\nadditional winget source — the winget CLI queries it exactly like the official Microsoft source.\nThe ",[171,176,177],{},"origin"," is the server you run: the admin UI, the winget Source API, and installer delivery.\nClients are unmodified ",[119,180,181],{},"winget"," installations — nothing installs on the Windows side beyond a\nsource registration.",[10,184,186],{"id":185},"what-you-need","What you need",[15,188,189,190,193],{},"A host that can run a container, a DNS name pointing at it, and trusted HTTPS. winget refuses REST\nsources that aren't served over HTTPS with a certificate the client trusts — including ",[119,191,192],{},"localhost","\non recent winget versions.",[10,195,197],{"id":196},"_1-run-the-origin-docker","1. Run the origin (Docker)",[15,199,200,201,204],{},"The Community edition runs from a single ",[119,202,203],{},"docker compose"," stack (app + PostgreSQL). With a domain\npointing at your host and ports 80\u002F443 open:",[112,206,210],{"className":207,"code":208,"language":209,"meta":117,"style":117},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","git clone \u003Copen-core-repo> kvellman && cd kvellman\ncp .env.deploy.example .env.deploy   # set DOMAIN, NUXT_SESSION_PASSWORD, POSTGRES_PASSWORD\ndocker compose --env-file .env.deploy up -d\n","bash",[119,211,212,249,264],{"__ignoreMap":117},[122,213,214,218,222,226,229,233,236,239,242,246],{"class":124,"line":125},[122,215,217],{"class":216},"sBMFI","git",[122,219,221],{"class":220},"sfazB"," clone",[122,223,225],{"class":224},"sMK4o"," \u003C",[122,227,228],{"class":220},"open-core-rep",[122,230,232],{"class":231},"sTEyZ","o",[122,234,235],{"class":224},">",[122,237,238],{"class":220}," kvellman",[122,240,241],{"class":224}," &&",[122,243,245],{"class":244},"s2Zo4"," cd",[122,247,248],{"class":220}," kvellman\n",[122,250,251,254,257,260],{"class":124,"line":140},[122,252,253],{"class":216},"cp",[122,255,256],{"class":220}," .env.deploy.example",[122,258,259],{"class":220}," .env.deploy",[122,261,263],{"class":262},"sHwdD","   # set DOMAIN, NUXT_SESSION_PASSWORD, POSTGRES_PASSWORD\n",[122,265,267,270,273,276,278,281],{"class":124,"line":266},3,[122,268,269],{"class":216},"docker",[122,271,272],{"class":220}," compose",[122,274,275],{"class":220}," --env-file",[122,277,259],{"class":220},[122,279,280],{"class":220}," up",[122,282,283],{"class":220}," -d\n",[15,285,286,287,290],{},"Database migrations run automatically on start. Open ",[119,288,289],{},"https:\u002F\u002Fyour-domain"," — the first visit\ncreates the admin account.",[10,292,294],{"id":293},"_2-create-a-site-token","2. Create a site token",[15,296,297,298,303],{},"In the web UI: Admin → Site tokens → create one. The token becomes part of the URL clients use to\nreach this source. See ",[299,300,302],"a",{"href":301},"\u002Fdocs\u002Fsites","Sites"," for what a site token actually controls.",[10,305,307],{"id":306},"_3-add-a-package","3. Add a package",[15,309,310],{},"Two ways to get a package into the catalogue:",[312,313,314,321],"ul",{},[315,316,317,320],"li",{},[171,318,319],{},"Import"," it from the upstream winget catalogue (Packages → Import), or",[315,322,323,326],{},[171,324,325],{},"Author"," it yourself: create the package and upload a manifest.",[15,328,329,330,333,334,337],{},"Either way, a new version isn't visible to clients until it's ",[171,331,332],{},"approved"," — open the version and\napprove it (Packages → your package → the pending version). This is the step people most often\nmiss: an imported package that shows up in the dashboard but returns nothing from ",[119,335,336],{},"winget search","\nalmost always just needs approval.",[10,339,341],{"id":340},"_4-add-the-source-on-a-client","4. Add the source on a client",[15,343,344,345,348],{},"winget requires ",[171,346,347],{},"HTTPS"," for a REST source. On a Windows client (PowerShell):",[112,350,352],{"className":114,"code":351,"language":116,"meta":117,"style":117},"winget source add --name kvellman --type Microsoft.Rest --arg \"https:\u002F\u002Fyour-domain\u002Fapi\u002F\u003Csite-token>\"\nwinget search --source kvellman \u003Cterm>\nwinget install --source kvellman \u003CPackage.Identifier>\n",[119,353,354,359,364],{"__ignoreMap":117},[122,355,356],{"class":124,"line":125},[122,357,358],{},"winget source add --name kvellman --type Microsoft.Rest --arg \"https:\u002F\u002Fyour-domain\u002Fapi\u002F\u003Csite-token>\"\n",[122,360,361],{"class":124,"line":140},[122,362,363],{},"winget search --source kvellman \u003Cterm>\n",[122,365,366],{"class":124,"line":266},[122,367,368],{},"winget install --source kvellman \u003CPackage.Identifier>\n",[15,370,371],{},"That's it — clients now install internal software through your own winget source.",[10,373,375],{"id":374},"next-steps","Next steps",[312,377,378,385,392,397,404,410],{},[315,379,380,384],{},[299,381,383],{"href":382},"\u002Fdocs\u002Fconcepts","Concepts"," — the vocabulary the rest of the docs use.",[315,386,387,391],{},[299,388,390],{"href":389},"\u002Fdocs\u002Fpackages","Packages & manifests"," — importing, overlays, approval, installer storage.",[315,393,394,396],{},[299,395,302],{"href":301}," — site tokens, URL placeholders, onboarding more clients.",[315,398,399,403],{},[299,400,402],{"href":401},"\u002Fdocs\u002Fusers","Users & roles"," — accounts, the audit log, SSO\u002FMFA.",[315,405,406,409],{},[299,407,5],{"href":408},"\u002Fdocs\u002Fnodes"," — serving remote locations from a local cache.",[315,411,412,416],{},[299,413,415],{"href":414},"\u002Fdocs\u002Fdeployment","Deployment"," — the reverse-proxy requirement, configuration, operations.",[136,418,419],{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":117,"searchDepth":140,"depth":140,"links":421},[422,423,424,425,426,427,428],{"id":165,"depth":140,"text":166},{"id":185,"depth":140,"text":186},{"id":196,"depth":140,"text":197},{"id":293,"depth":140,"text":294},{"id":306,"depth":140,"text":307},{"id":340,"depth":140,"text":341},{"id":374,"depth":140,"text":375},"Run a kvellman origin and get a Windows client installing from it in about ten minutes.",{},10,"\u002Fen\u002Fgetting-started",{"title":160,"description":429},"en\u002Fgetting-started","pjZGXbKzkR94D6J_DSqVnCtwNiQAYlwUEeQhpA3nfgw",{"id":437,"title":383,"body":438,"description":596,"extension":149,"meta":597,"nav":598,"navigation":152,"path":599,"seo":600,"stem":601,"__hash__":602},"docs\u002Fen\u002Fconcepts.md",{"type":7,"value":439,"toc":585},[440,443,449,453,472,476,487,491,506,510,516,520,537,541,558,562,570,574],[10,441,442],{"id":177},"Origin",[15,444,445,446,448],{},"The ",[171,447,177],{}," is the kvellman installation you run: the admin UI, the winget Source API, and\ninstaller delivery. One origin is a complete, self-contained system — everything else in these\ndocs (sites, nodes, users) is configured on top of it.",[10,450,452],{"id":451},"package-version-manifest","Package, version, manifest",[15,454,455,456,459,460,463,464,467,468,471],{},"A ",[171,457,458],{},"package"," has a stable identifier (e.g. ",[119,461,462],{},"Vendor.App",") and one or more ",[171,465,466],{},"versions",". Each\nversion's ",[171,469,470],{},"manifest"," describes it in the shapes winget itself defines — installers, switches,\ndependencies — and is stored as structured data, not as files on disk.",[10,473,475],{"id":474},"upstream-and-overlay","Upstream and overlay",[15,477,478,479,482,483,486],{},"When you import a package, its original manifest is kept untouched as the ",[171,480,481],{},"upstream"," state. Any\nchange you make lives as an ",[171,484,485],{},"overlay"," on top of it, so you can always see the difference from\nupstream and reset back to it.",[10,488,490],{"id":489},"overlay-template","Overlay template",[15,492,493,494,497,498,501,502,505],{},"An ",[171,495,496],{},"overlay template"," generalizes one overlay into a reusable rule — for example, an installer\nURL rewritten with ",[119,499,500],{},"$VERSION","\u002F",[119,503,504],{},"$ARCH"," placeholders — and applies it automatically to every future\nversion of that package, so you don't repeat the same edit by hand each release.",[10,507,509],{"id":508},"approval","Approval",[15,511,512,513,515],{},"A version is only returned to clients once it's ",[171,514,332],{},". This is enforced by the API itself,\nnot just hidden in the UI — an unapproved version simply isn't in the results winget receives,\nhowever it got into the catalogue.",[10,517,519],{"id":518},"site-and-site-token","Site and site token",[15,521,455,522,525,526,529,530,533,534,536],{},[171,523,524],{},"site"," is a named context: a name, a location, a default locale, a repository URL. Its\n",[171,527,528],{},"token"," is its address — clients reach it at ",[119,531,532],{},"https:\u002F\u002Fyour-origin\u002Fapi\u002F\u003Csite-token>",". A site's\nvalues resolve placeholders inside installer URLs, so one manifest can serve every location\ncorrectly. A site is not a tenant, not a separate catalogue, and nothing is installed anywhere to\ncreate one — see ",[299,535,302],{"href":301},".",[10,538,540],{"id":539},"users-and-roles","Users and roles",[15,542,543,544,547,548,551,552,555,556,536],{},"Every account has a role: ",[171,545,546],{},"viewer"," (read), ",[171,549,550],{},"reviewer"," (can edit and approve), or ",[171,553,554],{},"admin","\n(also manages users, sites, nodes and the license). See ",[299,557,402],{"href":401},[10,559,561],{"id":560},"edge-node","Edge node",[15,563,493,564,567,568,536],{},[171,565,566],{},"edge node"," is a component that runs at a remote location and serves winget clients there\nfrom a local cache of the origin's catalogue, managed centrally from the origin. See\n",[299,569,5],{"href":408},[10,571,573],{"id":572},"edition-and-entitlements","Edition and entitlements",[15,575,576,577,580,581,536],{},"The core platform is open source. Some components are licensed separately and activate when your\nlicense token carries the matching ",[171,578,579],{},"entitlement"," — see\n",[299,582,584],{"href":583},"\u002Fdocs\u002Feditions-licensing","Editions & licensing",{"title":117,"searchDepth":140,"depth":140,"links":586},[587,588,589,590,591,592,593,594,595],{"id":177,"depth":140,"text":442},{"id":451,"depth":140,"text":452},{"id":474,"depth":140,"text":475},{"id":489,"depth":140,"text":490},{"id":508,"depth":140,"text":509},{"id":518,"depth":140,"text":519},{"id":539,"depth":140,"text":540},{"id":560,"depth":140,"text":561},{"id":572,"depth":140,"text":573},"The object model and vocabulary the rest of the documentation assumes.",{},20,"\u002Fen\u002Fconcepts",{"title":383,"description":596},"en\u002Fconcepts","aNU7iJSRnAfSRxbAh8Eb7exmBMggMA6wJEIkTW0-_pQ",{"id":604,"title":390,"body":605,"description":756,"extension":149,"meta":757,"nav":758,"navigation":152,"path":759,"seo":760,"stem":761,"__hash__":762},"docs\u002Fen\u002Fpackages.md",{"type":7,"value":606,"toc":748},[607,611,614,628,650,654,657,661,671,675,687,695,702,706,718,722,725,745],[10,608,610],{"id":609},"where-packages-come-from","Where packages come from",[15,612,613],{},"Two ways to add a package to the catalogue:",[312,615,616,622],{},[315,617,618,621],{},[171,619,620],{},"Import from the upstream catalogue"," — Packages → Import, search the synced upstream index,\npick a version, import it.",[315,623,624,627],{},[171,625,626],{},"Author it yourself"," — create the package and upload manifest YAML directly, for internal-only\nsoftware that has no upstream entry.",[15,629,630,631,634,635,638,639,642,643,646,647,536],{},"The upstream index mirrors the community ",[119,632,633],{},"winget-pkgs"," repository on GitHub. Set ",[119,636,637],{},"GITHUB_TOKEN","\n(see ",[299,640,415],{"href":641},"\u002Fdocs\u002Fdeployment#configuration",") to raise the import rate limit against GitHub's\nAPI; ",[119,644,645],{},"CATALOG_SYNC_ENABLED"," keeps the searchable index itself refreshed on the interval set in\n",[119,648,649],{},"CATALOG_SYNC_INTERVAL_HOURS",[10,651,653],{"id":652},"validation","Validation",[15,655,656],{},"Every manifest is parsed and validated against the official winget JSON schemas — versions 1.0.0\nthrough 1.12.0 ship with the application. A manifest that doesn't validate is rejected before it's\nstored, with the schema violation reported back — the missing field, wrong type, or invalid enum\nvalue — so clients never receive a manifest winget itself can't read.",[10,658,660],{"id":659},"overlays","Overlays",[15,662,663,664,667,668,670],{},"The imported upstream manifest is kept exactly as received. Any edit you make — repointing an\n",[119,665,666],{},"InstallerUrl",", changing installer scope, correcting metadata — is stored as an ",[171,669,485],{}," on top\nof it. You can view the diff against upstream at any time, or reset a version back to the untouched\nupstream manifest.",[10,672,674],{"id":673},"overlay-templates","Overlay templates",[15,676,677,678,681,682,501,684,686],{},"Repeating the same overlay on every new release is tedious, so an overlay can be generalized into a\n",[171,679,680],{},"template",": write the edit once with ",[119,683,500],{},[119,685,504],{}," placeholders, and it's applied\nautomatically to every future version of that package.",[112,688,693],{"className":689,"code":691,"language":692},[690],"language-text","InstallerUrl: https:\u002F\u002Ffiles.example.internal\u002F$ARCH\u002FApp-$VERSION.msi\n","text",[119,694,691],{"__ignoreMap":117},[15,696,697,698,701],{},"Import ",[119,699,700],{},"App"," 2.3.0 today, write that template once, and version 2.4.0 six months from now already\nhas the right overlay applied when it lands.",[10,703,705],{"id":704},"version-history-and-approval","Version history and approval",[15,707,708,709,713,714,717],{},"Every change to a version is recorded and diffable, and lands in the\n",[299,710,712],{"href":711},"\u002Fdocs\u002Fusers#audit-log","audit log"," with the account that made it. A version reaches clients only\nafter a reviewer or admin ",[171,715,716],{},"approves"," it — the dashboard shows what's waiting. This is enforced in\nthe API, not only reflected in the UI.",[10,719,721],{"id":720},"installer-storage","Installer storage",[15,723,724],{},"Two ways for a manifest to point at an installer, and you choose per package:",[726,727,728,734],"ol",{},[315,729,730,733],{},[171,731,732],{},"Store it on the origin."," The file lives on the origin's own storage volume and is served\ndirectly from there. Its SHA-256 is recomputed on every delivery, so a file that changed\nunexpectedly on disk fails winget's hash check instead of silently installing.",[315,735,736,739,740,744],{},[171,737,738],{},"Point at a URL."," The manifest keeps the upstream vendor URL, or an internal one — including a\nlocation-specific share resolved through ",[299,741,743],{"href":742},"\u002Fdocs\u002Fsites#placeholders","site placeholders",". kvellman\nresolves the URL per site; it does not copy the file anywhere on your behalf.",[15,746,747],{},"kvellman does not mirror installers to S3, MinIO, or an SMB share on its own — if you want that\nshape, put the URL for it in the manifest and let the placeholder resolve it.",{"title":117,"searchDepth":140,"depth":140,"links":749},[750,751,752,753,754,755],{"id":609,"depth":140,"text":610},{"id":652,"depth":140,"text":653},{"id":659,"depth":140,"text":660},{"id":673,"depth":140,"text":674},{"id":704,"depth":140,"text":705},{"id":720,"depth":140,"text":721},"Import or author packages, validate, overlay, template, approve, and deliver installers.",{},30,"\u002Fen\u002Fpackages",{"title":390,"description":756},"en\u002Fpackages","JoHshm7jmMdINjvOImbJIakF3vrvPM7gPvxWiCemPNc",{"id":764,"title":302,"body":765,"description":999,"extension":149,"meta":1000,"nav":1001,"navigation":152,"path":1002,"seo":1003,"stem":1004,"__hash__":1005},"docs\u002Fen\u002Fsites.md",{"type":7,"value":766,"toc":990},[767,771,774,836,840,846,855,858,862,865,916,923,950,953,957,967,971,974,978,981,985,988],[10,768,770],{"id":769},"what-a-site-is","What a site is",[15,772,773],{},"A site is a named context, not a tenant or a piece of installed software:",[23,775,776,786],{},[26,777,778],{},[29,779,780,783],{},[32,781,782],{},"Field",[32,784,785],{},"What it's for",[39,787,788,796,804,816,826],{},[29,789,790,793],{},[44,791,792],{},"Name",[44,794,795],{},"A human-readable label (e.g. a building or region).",[29,797,798,801],{},[44,799,800],{},"Token",[44,802,803],{},"The URL segment clients use to reach this source.",[29,805,806,809],{},[44,807,808],{},"Location",[44,810,811,812,815],{},"Resolves the ",[119,813,814],{},"$LOCATION"," placeholder.",[29,817,818,821],{},[44,819,820],{},"Default locale",[44,822,811,823,815],{},[119,824,825],{},"$LANG",[29,827,828,831],{},[44,829,830],{},"Repository URL",[44,832,811,833,815],{},[119,834,835],{},"$REPO_URL",[10,837,839],{"id":838},"the-source-url","The source URL",[15,841,842,843,845],{},"Every site has its own source URL: ",[119,844,532],{},". Add it on a client\nexactly like any winget REST source:",[112,847,849],{"className":114,"code":848,"language":116,"meta":117,"style":117},"winget source add --name kvellman --type Microsoft.Rest --arg \"https:\u002F\u002Fyour-origin\u002Fapi\u002F\u003Csite-token>\"\n",[119,850,851],{"__ignoreMap":117},[122,852,853],{"class":124,"line":125},[122,854,848],{},[15,856,857],{},"The API implements the winget Source REST contract directly — no client-side plugin or agent is\ninvolved.",[10,859,861],{"id":860},"placeholders","Placeholders",[15,863,864],{},"An installer URL in a manifest can reference the current site:",[23,866,867,877],{},[26,868,869],{},[29,870,871,874],{},[32,872,873],{},"Placeholder",[32,875,876],{},"Resolves from",[39,878,879,888,898,907],{},[29,880,881,885],{},[44,882,883],{},[119,884,835],{},[44,886,887],{},"The site's repository URL",[29,889,890,895],{},[44,891,892],{},[119,893,894],{},"$SITE",[44,896,897],{},"The site's name",[29,899,900,904],{},[44,901,902],{},[119,903,814],{},[44,905,906],{},"The site's location",[29,908,909,913],{},[44,910,911],{},[119,912,825],{},[44,914,915],{},"The site's default locale",[15,917,918,919,922],{},"Example: one manifest, ",[119,920,921],{},"InstallerUrl: $REPO_URL\u002Fsoftware\u002F$ARCH\u002FApp.msi",", resolved for two sites:",[312,924,925,939],{},[315,926,927,928,931,932,935,936],{},"Site ",[119,929,930],{},"munich"," with repository URL ",[119,933,934],{},"https:\u002F\u002Ffiles.munich.internal"," →\n",[119,937,938],{},"https:\u002F\u002Ffiles.munich.internal\u002Fsoftware\u002Fx64\u002FApp.msi",[315,940,927,941,931,944,935,947],{},[119,942,943],{},"austin",[119,945,946],{},"https:\u002F\u002Ffiles.austin.internal",[119,948,949],{},"https:\u002F\u002Ffiles.austin.internal\u002Fsoftware\u002Fx64\u002FApp.msi",[15,951,952],{},"Same package, same manifest, the correct file for each location.",[10,954,956],{"id":955},"onboarding-clients","Onboarding clients",[15,958,959,960,963,964,536],{},"The admin UI's Instructions page renders the exact ",[119,961,962],{},"winget source add"," command for a given site —\nsend that link rather than typing the token by hand. To remove a source on a client:\n",[119,965,966],{},"winget source remove --name kvellman",[10,968,970],{"id":969},"rotating-and-revoking-tokens","Rotating and revoking tokens",[15,972,973],{},"Admin → Site tokens. Add the replacement source on clients before you revoke the old token —\nrevoking breaks every client still using that URL immediately.",[10,975,977],{"id":976},"what-selects-a-site","What selects a site",[15,979,980],{},"Clients select a site purely by the URL they use — there's no separate subnet- or\ncertificate-based selection today. If you need different behaviour per network segment, control\nthat at your reverse proxy or firewall, and give each segment its own site token and URL.",[10,982,984],{"id":983},"who-can-reach-it","Who can reach it",[15,986,987],{},"The site token is an address, not a credential — the winget Source API has to stay reachable\nwithout a login, because winget itself sends no authentication. Anyone who can reach the URL can\nsearch the source and download its installers. If that's not acceptable for a given site, restrict\nit at the network layer or your reverse proxy, not by treating the token as secret.",[136,989,138],{},{"title":117,"searchDepth":140,"depth":140,"links":991},[992,993,994,995,996,997,998],{"id":769,"depth":140,"text":770},{"id":838,"depth":140,"text":839},{"id":860,"depth":140,"text":861},{"id":955,"depth":140,"text":956},{"id":969,"depth":140,"text":970},{"id":976,"depth":140,"text":977},{"id":983,"depth":140,"text":984},"What a site is, the source URL, URL placeholders, and onboarding clients.",{},40,"\u002Fen\u002Fsites",{"title":302,"description":999},"en\u002Fsites","RnFG3_U_ePh4emoGwaqPQbeSO6_01ObtcBhf-fob5Tw",{"id":1007,"title":402,"body":1008,"description":1156,"extension":149,"meta":1157,"nav":1158,"navigation":152,"path":1159,"seo":1160,"stem":1161,"__hash__":1162},"docs\u002Fen\u002Fusers.md",{"type":7,"value":1009,"toc":1149},[1010,1014,1017,1021,1118,1121,1125,1128,1132,1135,1139,1142],[10,1011,1013],{"id":1012},"first-run","First run",[15,1015,1016],{},"The first time you open the origin, the setup screen creates the initial admin account. Sign-in\nafter that is a local account with a session cookie.",[10,1018,1020],{"id":1019},"roles","Roles",[23,1022,1023,1038],{},[26,1024,1025],{},[29,1026,1027,1029,1032,1035],{},[32,1028],{},[32,1030,1031],{},"Viewer",[32,1033,1034],{},"Reviewer",[32,1036,1037],{},"Admin",[39,1039,1040,1052,1063,1074,1085,1096,1107],{},[29,1041,1042,1045,1048,1050],{},[44,1043,1044],{},"Read the catalogue",[44,1046,1047],{},"✓",[44,1049,1047],{},[44,1051,1047],{},[29,1053,1054,1057,1059,1061],{},[44,1055,1056],{},"Create & edit packages",[44,1058],{},[44,1060,1047],{},[44,1062,1047],{},[29,1064,1065,1068,1070,1072],{},[44,1066,1067],{},"Approve versions",[44,1069],{},[44,1071,1047],{},[44,1073,1047],{},[29,1075,1076,1079,1081,1083],{},[44,1077,1078],{},"Manage sites",[44,1080],{},[44,1082,1047],{},[44,1084,1047],{},[29,1086,1087,1090,1092,1094],{},[44,1088,1089],{},"Manage users",[44,1091],{},[44,1093],{},[44,1095,1047],{},[29,1097,1098,1101,1103,1105],{},[44,1099,1100],{},"Manage edge nodes",[44,1102],{},[44,1104],{},[44,1106,1047],{},[29,1108,1109,1112,1114,1116],{},[44,1110,1111],{},"Manage the license",[44,1113],{},[44,1115],{},[44,1117,1047],{},[15,1119,1120],{},"Roles are enforced on the server, not just reflected in the UI — a viewer's session can't perform a\nreviewer action even by calling the API directly.",[10,1122,1124],{"id":1123},"managing-users","Managing users",[15,1126,1127],{},"Admin → Users: create an account, change its role, or remove it.",[10,1129,1131],{"id":1130},"audit-log","Audit log",[15,1133,1134],{},"Every approval and administrative action is recorded in an append-only audit log — who did what,\nand when. Typical entries: a version approved or reset to upstream, a site token created or\nrevoked, a node enrolled or revoked, a user's role changed, a license token replaced. Any\nauthenticated user can read it; changing what's audited is not configurable.",[10,1136,1138],{"id":1137},"sso-and-mfa","SSO and MFA",[15,1140,1141],{},"The application ships with local accounts.",[84,1143,1144],{"edition":86},[15,1145,1146,1147,536],{},"Single sign-on (OIDC\u002FSAML) and multi-factor authentication (TOTP) are provided by licensed plugins,\nwhich the application loads once your license carries the matching entitlement. See\n",[299,1148,584],{"href":583},{"title":117,"searchDepth":140,"depth":140,"links":1150},[1151,1152,1153,1154,1155],{"id":1012,"depth":140,"text":1013},{"id":1019,"depth":140,"text":1020},{"id":1123,"depth":140,"text":1124},{"id":1130,"depth":140,"text":1131},{"id":1137,"depth":140,"text":1138},"Accounts, roles, the audit log, and how SSO\u002FMFA fit in.",{},50,"\u002Fen\u002Fusers",{"title":402,"description":1156},"en\u002Fusers","QGIwkCkuzLSyOuBjZ-3Ma3U37Fh4D8h8iy0jbQFUdxI",{"id":4,"title":5,"body":1164,"description":148,"extension":149,"meta":1254,"nav":151,"navigation":152,"path":153,"seo":1255,"stem":155,"__hash__":156},{"type":7,"value":1165,"toc":1246},[1166,1168,1170,1172,1214,1216,1220,1222,1224,1226,1228,1230,1232,1240,1242,1244],[10,1167,13],{"id":12},[15,1169,17],{},[10,1171,21],{"id":20},[23,1173,1174,1182],{},[26,1175,1176],{},[29,1177,1178,1180],{},[32,1179,34],{},[32,1181,37],{},[39,1183,1184,1190,1196,1202,1208],{},[29,1185,1186,1188],{},[44,1187,46],{},[44,1189,49],{},[29,1191,1192,1194],{},[44,1193,54],{},[44,1195,57],{},[29,1197,1198,1200],{},[44,1199,62],{},[44,1201,65],{},[29,1203,1204,1206],{},[44,1205,70],{},[44,1207],{},[29,1209,1210,1212],{},[44,1211,77],{},[44,1213],{},[15,1215,82],{},[84,1217,1218],{"edition":86},[15,1219,89],{},[10,1221,93],{"id":92},[15,1223,96],{},[10,1225,100],{"id":99},[15,1227,103],{},[10,1229,107],{"id":106},[15,1231,110],{},[112,1233,1234],{"className":114,"code":115,"language":116,"meta":117,"style":117},[119,1235,1236],{"__ignoreMap":117},[122,1237,1238],{"class":124,"line":125},[122,1239,115],{},[10,1241,131],{"id":130},[15,1243,134],{},[136,1245,138],{},{"title":117,"searchDepth":140,"depth":140,"links":1247},[1248,1249,1250,1251,1252,1253],{"id":12,"depth":140,"text":13},{"id":20,"depth":140,"text":21},{"id":92,"depth":140,"text":93},{"id":99,"depth":140,"text":100},{"id":106,"depth":140,"text":107},{"id":130,"depth":140,"text":131},{},{"title":5,"description":148},{"id":1257,"title":415,"body":1258,"description":1675,"extension":149,"meta":1676,"nav":1677,"navigation":152,"path":1678,"seo":1679,"stem":1680,"__hash__":1681},"docs\u002Fen\u002Fdeployment.md",{"type":7,"value":1259,"toc":1660},[1260,1264,1284,1288,1294,1298,1301,1304,1329,1332,1336,1341,1352,1381,1385,1395,1420,1424,1431,1499,1503,1612,1616,1623,1627,1630,1634,1637,1641,1647,1657],[10,1261,1263],{"id":1262},"what-the-application-needs","What the application needs",[312,1265,1266,1272,1278],{},[315,1267,1268,1271],{},[171,1269,1270],{},"PostgreSQL"," — the origin's entire state.",[315,1273,1274,1277],{},[171,1275,1276],{},"A persistent volume"," — for installers you choose to store on the origin.",[315,1279,1280,1283],{},[171,1281,1282],{},"Something in front that terminates HTTPS."," The application itself is a plain HTTP server on\nport 3000; it does not handle TLS.",[10,1285,1287],{"id":1286},"https-is-not-optional","HTTPS is not optional",[15,1289,1290,1291,1293],{},"winget refuses a REST source that isn't served over HTTPS with a certificate the client trusts —\nincluding ",[119,1292,192],{}," on recent winget versions. An internal CA is fine as long as your clients\ntrust it; an untrusted self-signed certificate is not.",[10,1295,1297],{"id":1296},"the-reverse-proxy","The reverse proxy",[15,1299,1300],{},"kvellman listens on port 3000 and speaks plain HTTP. It does not terminate TLS, and it does not\ncare what sits in front of it. Anything that can terminate HTTPS and forward to an HTTP upstream\nworks: nginx, HAProxy, Apache, an appliance, your existing ingress.",[15,1302,1303],{},"Whatever you use must:",[312,1305,1306,1309,1312,1323,1326],{},[315,1307,1308],{},"forward to the application's port 3000",[315,1310,1311],{},"serve a certificate the clients trust",[315,1313,1314,1315,1318,1319,1322],{},"pass the original host and protocol through (",[119,1316,1317],{},"Host",", ",[119,1320,1321],{},"X-Forwarded-Proto",")",[315,1324,1325],{},"not buffer or limit response bodies — installers are large",[315,1327,1328],{},"allow long-running downloads (generous read\u002Fsend timeouts)",[15,1330,1331],{},"That's the entire integration surface.",[10,1333,1335],{"id":1334},"worked-examples","Worked examples",[1337,1338,1340],"h3",{"id":1339},"a-compose-stack-with-a-bundled-proxy","A compose stack with a bundled proxy",[15,1342,1343,1344,1347,1348,1351],{},"The default ",[119,1345,1346],{},"docker-compose.yml"," bundles ",[171,1349,1350],{},"Caddy",", which obtains a Let's Encrypt certificate\nautomatically. Use it when the host is publicly reachable and you don't already run a proxy.",[112,1353,1355],{"className":207,"code":1354,"language":209,"meta":117,"style":117},"cp .env.deploy.example .env.deploy   # set DOMAIN, NUXT_SESSION_PASSWORD, POSTGRES_PASSWORD\ndocker compose --env-file .env.deploy up -d\n",[119,1356,1357,1367],{"__ignoreMap":117},[122,1358,1359,1361,1363,1365],{"class":124,"line":125},[122,1360,253],{"class":216},[122,1362,256],{"class":220},[122,1364,259],{"class":220},[122,1366,263],{"class":262},[122,1368,1369,1371,1373,1375,1377,1379],{"class":124,"line":140},[122,1370,269],{"class":216},[122,1372,272],{"class":220},[122,1374,275],{"class":220},[122,1376,259],{"class":220},[122,1378,280],{"class":220},[122,1380,283],{"class":220},[1337,1382,1384],{"id":1383},"routing-through-an-ingress-you-already-run","Routing through an ingress you already run",[15,1386,1387,1390,1391,1394],{},[119,1388,1389],{},"docker-compose.traefik.yml"," ships without a bundled proxy — the application joins your existing\n",[171,1392,1393],{},"Traefik"," network and is routed by labels. Use it when ports 80\u002F443 already belong to something\nelse.",[112,1396,1398],{"className":207,"code":1397,"language":209,"meta":117,"style":117},"docker compose -f docker-compose.traefik.yml --env-file .env.deploy up -d\n",[119,1399,1400],{"__ignoreMap":117},[122,1401,1402,1404,1406,1409,1412,1414,1416,1418],{"class":124,"line":125},[122,1403,269],{"class":216},[122,1405,272],{"class":220},[122,1407,1408],{"class":220}," -f",[122,1410,1411],{"class":220}," docker-compose.traefik.yml",[122,1413,275],{"class":220},[122,1415,259],{"class":220},[122,1417,280],{"class":220},[122,1419,283],{"class":220},[1337,1421,1423],{"id":1422},"any-other-proxy","Any other proxy",[15,1425,1426,1427,1430],{},"The application doesn't care. Point any HTTPS-terminating proxy at port 3000 — for example\n",[171,1428,1429],{},"nginx",":",[112,1432,1435],{"className":1433,"code":1434,"language":1429,"meta":117,"style":117},"language-nginx shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","server {\n  listen 443 ssl;\n  server_name your-domain;\n  location \u002F {\n    proxy_pass http:\u002F\u002Fkvellman:3000;\n    proxy_set_header Host $host;\n    proxy_set_header X-Forwarded-Proto $scheme;\n    proxy_request_buffering off;\n    client_max_body_size 0;\n  }\n}\n",[119,1436,1437,1442,1447,1452,1458,1464,1470,1476,1482,1488,1493],{"__ignoreMap":117},[122,1438,1439],{"class":124,"line":125},[122,1440,1441],{},"server {\n",[122,1443,1444],{"class":124,"line":140},[122,1445,1446],{},"  listen 443 ssl;\n",[122,1448,1449],{"class":124,"line":266},[122,1450,1451],{},"  server_name your-domain;\n",[122,1453,1455],{"class":124,"line":1454},4,[122,1456,1457],{},"  location \u002F {\n",[122,1459,1461],{"class":124,"line":1460},5,[122,1462,1463],{},"    proxy_pass http:\u002F\u002Fkvellman:3000;\n",[122,1465,1467],{"class":124,"line":1466},6,[122,1468,1469],{},"    proxy_set_header Host $host;\n",[122,1471,1473],{"class":124,"line":1472},7,[122,1474,1475],{},"    proxy_set_header X-Forwarded-Proto $scheme;\n",[122,1477,1479],{"class":124,"line":1478},8,[122,1480,1481],{},"    proxy_request_buffering off;\n",[122,1483,1485],{"class":124,"line":1484},9,[122,1486,1487],{},"    client_max_body_size 0;\n",[122,1489,1490],{"class":124,"line":431},[122,1491,1492],{},"  }\n",[122,1494,1496],{"class":124,"line":1495},11,[122,1497,1498],{},"}\n",[10,1500,1502],{"id":1501},"configuration","Configuration",[23,1504,1505,1515],{},[26,1506,1507],{},[29,1508,1509,1512],{},[32,1510,1511],{},"Variable",[32,1513,1514],{},"Purpose",[39,1516,1517,1527,1540,1550,1560,1574,1585,1594],{},[29,1518,1519,1524],{},[44,1520,1521],{},[119,1522,1523],{},"DOMAIN",[44,1525,1526],{},"Public hostname (used by the bundled Caddy compose file for its certificate).",[29,1528,1529,1534],{},[44,1530,1531],{},[119,1532,1533],{},"NUXT_SESSION_PASSWORD",[44,1535,1536,1537,536],{},"Session-cookie sealing secret, ≥32 chars. Generate with ",[119,1538,1539],{},"openssl rand -hex 32",[29,1541,1542,1547],{},[44,1543,1544],{},[119,1545,1546],{},"POSTGRES_PASSWORD",[44,1548,1549],{},"Password for the bundled PostgreSQL container.",[29,1551,1552,1557],{},[44,1553,1554],{},[119,1555,1556],{},"KVELLMAN_IMAGE",[44,1558,1559],{},"A prebuilt image to pull instead of building on the host.",[29,1561,1562,1571],{},[44,1563,1564,1567,1568],{},[119,1565,1566],{},"TELEMETRY_ENABLED"," \u002F ",[119,1569,1570],{},"TELEMETRY_RETENTION_DAYS",[44,1572,1573],{},"Usage telemetry, off by default.",[29,1575,1576,1582],{},[44,1577,1578,1567,1580],{},[119,1579,645],{},[119,1581,649],{},[44,1583,1584],{},"Scheduled upstream-catalog sync, off by default.",[29,1586,1587,1591],{},[44,1588,1589],{},[119,1590,637],{},[44,1592,1593],{},"Raises the winget-pkgs import rate limit.",[29,1595,1596,1607],{},[44,1597,1598,1567,1601,1567,1604],{},[119,1599,1600],{},"TRAEFIK_NETWORK",[119,1602,1603],{},"TRAEFIK_ENTRYPOINT",[119,1605,1606],{},"TRAEFIK_CERTRESOLVER",[44,1608,1609,1610,536],{},"Only for ",[119,1611,1389],{},[10,1613,1615],{"id":1614},"running-it-without-the-compose-files","Running it without the compose files",[15,1617,1618,1619,1622],{},"Bringing your own PostgreSQL and orchestration is fine: run the image, point ",[119,1620,1621],{},"DATABASE_URL"," at your\ndatabase, and expose port 3000 to your proxy. Migrations run automatically at startup — there's no\nseparate migration step to script.",[10,1624,1626],{"id":1625},"backups-and-upgrades","Backups and upgrades",[15,1628,1629],{},"An installation's entire state is the PostgreSQL database plus the installer volume — back up both.\nUpgrading is pulling a newer image tag and restarting; migrations run on start.",[10,1631,1633],{"id":1632},"telemetry","Telemetry",[15,1635,1636],{},"Off by default, enabled by environment variable. Records searches, manifest fetches and installer\ndownloads, rolled up hourly inside the application process itself, with raw events purged after the\nretention period you configure. No queue, cache, or additional service is required to run it. The\nrolled-up numbers surface in the admin UI as dashboards, broken down by site and by winget client\nversion.",[10,1638,1640],{"id":1639},"air-gapped-operation","Air-gapped operation",[15,1642,1643,1644,1646],{},"The application makes no outbound calls to run, and license verification is entirely local, so it\noperates fully disconnected. The upstream-catalog sync and manifest import from GitHub do need\ninternet access — leave ",[119,1645,645],{}," off and import manifests by hand if the host has\nnone.",[1648,1649,1650],"blockquote",{},[15,1651,1652,1653,1656],{},"The full step-by-step guide (origin, edge node under WSL, TLS for LAN\u002Flocalhost, the registry\nworkflow) is in ",[119,1654,1655],{},"DEPLOYMENT.md"," in the open-core repository — this page covers the shape of it.",[136,1658,1659],{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":117,"searchDepth":140,"depth":140,"links":1661},[1662,1663,1664,1665,1670,1671,1672,1673,1674],{"id":1262,"depth":140,"text":1263},{"id":1286,"depth":140,"text":1287},{"id":1296,"depth":140,"text":1297},{"id":1334,"depth":140,"text":1335,"children":1666},[1667,1668,1669],{"id":1339,"depth":266,"text":1340},{"id":1383,"depth":266,"text":1384},{"id":1422,"depth":266,"text":1423},{"id":1501,"depth":140,"text":1502},{"id":1614,"depth":140,"text":1615},{"id":1625,"depth":140,"text":1626},{"id":1632,"depth":140,"text":1633},{"id":1639,"depth":140,"text":1640},"Run the origin behind any HTTPS-terminating reverse proxy — the requirement, and three worked examples.",{},70,"\u002Fen\u002Fdeployment",{"title":415,"description":1675},"en\u002Fdeployment","DNv1UDjcZiEnvna8amUePUGntxfJfehLSk4vt4kXfcM",{"id":1683,"title":1684,"body":1685,"description":1778,"extension":149,"meta":1779,"nav":1780,"navigation":152,"path":1781,"seo":1782,"stem":1783,"__hash__":1784},"docs\u002Fen\u002Farchitecture.md","Architecture",{"type":7,"value":1686,"toc":1769},[1687,1691,1694,1714,1717,1721,1724,1728,1735,1739,1744,1748,1755,1759,1762,1766],[10,1688,1690],{"id":1689},"request-path","Request path",[15,1692,1693],{},"A winget client talks HTTPS to your reverse proxy, which forwards plain HTTP to the origin. The\norigin reads and writes PostgreSQL and, for installers stored on it, its own storage volume.",[312,1695,1696,1702,1708],{},[315,1697,1698,1701],{},[119,1699,1700],{},"GET \u002Fapi\u002F{siteToken}\u002Finformation"," — source metadata and supported versions.",[315,1703,1704,1707],{},[119,1705,1706],{},"POST \u002Fapi\u002F{siteToken}\u002FmanifestSearch"," — full-text search.",[315,1709,1710,1713],{},[119,1711,1712],{},"GET \u002Fapi\u002F{siteToken}\u002FpackageManifests\u002F{id}"," — manifest delivery with server-side placeholder\nresolution.",[15,1715,1716],{},"Only approved versions are ever returned.",[10,1718,1720],{"id":1719},"data-model","Data model",[15,1722,1723],{},"Packages, versions and manifests are stored relationally, in shapes that mirror winget's own\nmanifest schema, alongside the untouched upstream snapshot and any overlay on top. Sites, edge\nnodes, users, the audit log and telemetry events live in the same database. One database is the\nentire state of an installation, plus the installer volume.",[10,1725,1727],{"id":1726},"installer-delivery","Installer delivery",[15,1729,1730,1731,1734],{},"Installers stored on the origin are served from its own volume at ",[119,1732,1733],{},"\u002Fdl\u002F...",", with the SHA-256\nrecomputed on every delivery.",[10,1736,1738],{"id":1737},"node-control-plane","Node control plane",[15,1740,1741,1742,536],{},"The origin enrolls, scopes and monitors edge nodes; it doesn't run the node itself. See\n",[299,1743,5],{"href":408},[10,1745,1747],{"id":1746},"extension-model","Extension model",[15,1749,1750,1751,1754],{},"Authentication providers register through a plugin host and are gated on entitlements read from the\nlicense token — a plugin only activates once the matching entitlement is present.\n",[119,1752,1753],{},"@kvellman\u002Fplugin-sdk"," is the public type surface for building against it.",[10,1756,1758],{"id":1757},"stack","Stack",[15,1760,1761],{},"Nuxt 4 (Vue 3 + Nitro), TypeScript, PostgreSQL with Drizzle ORM, Zod and the official winget JSON\nschemas (via ajv) for manifest validation, Nuxt UI, English and German interface. On-premise only —\nself-hostable including air-gapped operation.",[10,1763,1765],{"id":1764},"scope-of-an-installation","Scope of an installation",[15,1767,1768],{},"One application process handles requests, installer delivery, and scheduled work (telemetry\nrollup, catalog sync). Installers live on a local volume or are referenced by URL. Accounts are\nlocal to the installation in the core edition. An installation's components are the application\ncontainer and a database.",{"title":117,"searchDepth":140,"depth":140,"links":1770},[1771,1772,1773,1774,1775,1776,1777],{"id":1689,"depth":140,"text":1690},{"id":1719,"depth":140,"text":1720},{"id":1726,"depth":140,"text":1727},{"id":1737,"depth":140,"text":1738},{"id":1746,"depth":140,"text":1747},{"id":1757,"depth":140,"text":1758},{"id":1764,"depth":140,"text":1765},"Request path, data model, delivery, the extension model, and the scope of a single installation.",{},80,"\u002Fen\u002Farchitecture",{"title":1684,"description":1778},"en\u002Farchitecture","nHncWr5TAyDtM-LAsLEMm-EqgVjGLUI8rX-YF5Se6oU",{"id":1786,"title":584,"body":1787,"description":1965,"extension":149,"meta":1966,"nav":1967,"navigation":152,"path":1968,"seo":1969,"stem":1970,"__hash__":1971},"docs\u002Fen\u002Feditions-licensing.md",{"type":7,"value":1788,"toc":1960},[1789,1793,1808,1814,1819,1885,1893,1897,1904,1937,1940,1944,1947],[10,1790,1792],{"id":1791},"open-core","Open core",[15,1794,1795,1796,1799,1800,1803,1804,1807],{},"kvellman is ",[171,1797,1798],{},"open core",". The platform — including multi-user accounts with roles and the\napproval workflow — is open source under ",[171,1801,1802],{},"Apache-2.0",". A small number of components are\n",[171,1805,1806],{},"licensed plugins"," that activate only with a valid Enterprise entitlement.",[84,1809,1811],{"edition":1810},"community",[15,1812,1813],{},"Everything you need to run a production origin: the winget Source API, manifests, overlays and upstream import, multi-user accounts with viewer\u002Freviewer\u002Fadmin roles, the approval workflow and audit log, and air-gapped operation.",[84,1815,1816],{"edition":86},[15,1817,1818],{},"Adds SSO (OIDC) and MFA (TOTP) as licensed auth plugins, and the edge node component for serving additional locations from a local cache.",[23,1820,1821,1833],{},[26,1822,1823],{},[29,1824,1825,1827,1830],{},[32,1826],{},[32,1828,1829],{},"Community",[32,1831,1832],{},"Enterprise",[39,1834,1835,1845,1856,1866,1877],{},[29,1836,1837,1840,1842],{},[44,1838,1839],{},"License",[44,1841,1802],{},[44,1843,1844],{},"Commercial subscription",[29,1846,1847,1850,1853],{},[44,1848,1849],{},"Accounts",[44,1851,1852],{},"Multi-user, roles (viewer\u002Freviewer\u002Fadmin)",[44,1854,1855],{},"Same, plus SSO (OIDC) & MFA (TOTP)",[29,1857,1858,1861,1864],{},[44,1859,1860],{},"Approval workflow & audit log",[44,1862,1863],{},"Included",[44,1865,1863],{},[29,1867,1868,1871,1874],{},[44,1869,1870],{},"Additional locations",[44,1872,1873],{},"—",[44,1875,1876],{},"Edge node component",[29,1878,1879,1881,1883],{},[44,1880,1640],{},[44,1882,1863],{},[44,1884,1863],{},[15,1886,1887,1888,1892],{},"See ",[299,1889,1891],{"href":1890},"\u002Fpricing","pricing"," for the full comparison.",[10,1894,1896],{"id":1895},"how-licensing-works","How licensing works",[15,1898,1899,1900,1903],{},"Enterprise features are gated by an ",[171,1901,1902],{},"offline, signature-based"," license — no activation server, so\nit works air-gapped:",[726,1905,1906,1913,1920,1934],{},[315,1907,1908,1909,1912],{},"A license is an ",[171,1910,1911],{},"Ed25519-signed token"," issued by the vendor.",[315,1914,1915,1916,1919],{},"The product ships only the ",[171,1917,1918],{},"public key"," and verifies the token locally.",[315,1921,1922,1923,1926,1927,1318,1930,1933],{},"The token lists the customer and the active ",[171,1924,1925],{},"entitlements"," (e.g. ",[119,1928,1929],{},"sso",[119,1931,1932],{},"mfa",") and an expiry.",[315,1935,1936],{},"An admin pastes the token in the UI; matching plugins activate immediately — no restart, no\ninternet required.",[15,1938,1939],{},"Because verification is local, even the full open-source code cannot forge a valid license without\nthe vendor's private key.",[10,1941,1943],{"id":1942},"developer-packages","Developer packages",[15,1945,1946],{},"The shared, public building blocks are on npm under Apache-2.0:",[312,1948,1949,1955],{},[315,1950,1951,1954],{},[119,1952,1953],{},"@kvellman\u002Fwinget-contract"," — the winget REST\u002Ftypes contract shared by origin and nodes.",[315,1956,1957,1959],{},[119,1958,1753],{}," — types for building auth\u002Fentitlement plugins.",{"title":117,"searchDepth":140,"depth":140,"links":1961},[1962,1963,1964],{"id":1791,"depth":140,"text":1792},{"id":1895,"depth":140,"text":1896},{"id":1942,"depth":140,"text":1943},"Open-core model — what's in the Community core vs. licensed Enterprise components, and how licensing works.",{},90,"\u002Fen\u002Feditions-licensing",{"title":584,"description":1965},"en\u002Feditions-licensing","RzwmkpON0esr1255nKGd8l2KTghRb5VK2p0MGTK3k9w",1787516841130]