MeshWeaver ships a .NET project template — package id MeshWeaver.MemexTemplate, short name meshweaver-memex — that scaffolds a complete, runnable portal in one command. dotnet new meshweaver-memex -o MyProject produces a working solution with sample data, authentication, AI integration, and both monolith and distributed deployment options — ready to run in under a minute.

Why Use the Template?

Building a MeshWeaver portal from scratch means wiring up message hubs, layout areas, authentication, graph nodes, access control, and Aspire orchestration. The template handles all of that up front, giving you:

Quick Start

1. Install the Template

From NuGet:

dotnet new install MeshWeaver.MemexTemplate

Or build it from source (useful when working on the template itself). The generator lives in MeshWeaver.Plugins, not here: the six projects the template ships are split across the two repositories — that repo holds Memex.Portal.Monolith, Memex.AppHost and Memex.Portal.Distributed, while Memex.Portal.Shared, Memex.Database.Migration and Memex.Portal.ServiceDefaults stayed here, along with the sample data and the Directory.Packages.props the template's versions are read from. So it needs BOTH trees, and takes the platform checkout as --core:

# from a MeshWeaver.Plugins checkout, with a MeshWeaver checkout alongside
dotnet run tools/generate-memex-template.cs -- <version> . dist/templates \
    --core ../MeshWeaver
dotnet new install dist/templates/                  # or install the produced .nupkg

🚨 The published package has no GUI, and that is deliberate. Memex.Portal.Gui carries the portal's pages, the dev-login screen and the whole portal composition, and it lives only in the private MeshWeaver.Plugins repository — so copying it into a package published to public nuget.org would publish private source irreversibly. Both hosts therefore reference it from a conditioned ItemGroup and compile without it (#3653), and the generator drops the reference it is not shipping. What the flagless command above scaffolds is a headless mesh host: it composes the mesh, mounts module static assets and module endpoints, and answers the health probes — no pages, no authentication schemes, no MCP/SignalR/gRPC-web. The generated README says so.

--with-gui still exists and still copies Memex.Portal.Gui, for generating a template inside the private repository. It must never be passed by a lane that publishes, and the generator's own message says why.

2. Scaffold a New Project

dotnet new meshweaver-memex -o MyProject

This creates a MyProject/ directory with all projects renamed from Memex to MyProject.

3. Run the Monolith Portal

dotnet run --project MyProject/MyProject.Portal.Monolith

Open the URL shown in the console (the generated README says https://localhost:7122). The dev login page lists available users — click any name to sign in immediately.

The template does not ship a Properties/launchSettings.json (it is gitignored in the source tree the generator copies from), so dotnet run uses the ASP.NET defaults unless you add one. Set ASPNETCORE_ENVIRONMENT=Development yourself — either by adding a launchSettings.json or with --environment Development — otherwise the portal starts without the dev configuration it needs.

4. Run with Aspire (Distributed)

dotnet run --project MyProject/aspire/MyProject.AppHost

This launches the Aspire dashboard together with PostgreSQL (via Docker), the distributed portal with an Orleans silo, and the database migration service.

What Gets Generated

MyProject/
├── MyProject.slnx                          # Solution file
├── MyProject.Portal.Monolith/              # Standalone portal (no external deps)
│   ├── Program.cs                          # Entry point
│   └── appsettings.Development.json        # Graph storage paths, AI config
├── MyProject.Portal.Shared/                # Shared Razor UI, auth, configuration
│   ├── Pages/                              # DevLogin, Onboarding, portal pages
│   ├── Authentication/                     # DevAuthController, middleware
│   └── MyProjectConfiguration.cs           # Hub setup, AddGraph(), AddDocumentation()
├── aspire/
│   ├── MyProject.AppHost/                  # Aspire orchestrator
│   ├── MyProject.Portal.Distributed/       # Portal with Orleans silo
│   ├── MyProject.Database.Migration/       # Schema migration (run-to-completion)
│   └── MyProject.Portal.ServiceDefaults/   # Health, telemetry defaults
├── samples/Graph/Data/                     # Sample data loaded by AddGraph()
│   ├── ACME/                               # Insurance company demo
│   │   ├── Project/                        # Projects
│   │   ├── Article/ ProductLaunch/         # Sample content nodes
│   │   ├── Documentation/                  # ACME-specific documentation
│   │   ├── User/                           # 3 org-scoped users (Oliver, Paul, Quinn)
│   │   └── _Access/                        # Partition-level access assignments
│   └── User/                               # Top-level login users
│       ├── Alice.json  Bob.json  …         # Sample users (Roland/Samuel are excluded by the generator)
│       └── _Access/                        # Global access assignments
├── Directory.Build.props                   # MSBuild properties
├── Directory.Packages.props                # Centralized NuGet versions
└── nuget.config                            # Package sources

Template Architecture

Two User Scopes

The template ships users at two levels, mirroring MeshWeaver's built-in user convention:

Scope Path Purpose
Global User/Admin, User/Alice, User/Bob Portal-wide login users with namespace: "User"
Partition ACME/User/Oliver, ACME/User/Paul, ACME/User/Quinn Organization-scoped users with namespace: "ACME/User"

The DevLogin page lists users through AccessSubjectQueries.Users — the one canonical users query, nodeType:User namespace:"". 🚨 Do not hand-roll nodeType:User namespace:User: that legacy shape targets the pre-V27 user schema, which no longer exists, and it silently returns zero users (issue #213). Always reference AccessSubjectQueries.Users rather than re-typing a query.

Access Control

Every login user needs an AccessAssignment node that grants a role. These live under User/_Access/:

{
  "id": "Admin_Access",
  "namespace": "User/_Access",
  "nodeType": "AccessAssignment",
  "content": {
    "$type": "AccessAssignment",
    "accessObject": "Admin",
    "displayName": "Admin",
    "roles": [{ "role": "Viewer" }]
  }
}

Without an access assignment, a user can log in but receives "Access denied" on every page. In the sample data, User/_Access/ carries the global assignments and ACME/_Access/ the partition-scoped ones — all of them read-only (Viewer).

🚨 A shipped _Access file may never confer WRITE (Admin, Editor, PlatformAdmin, or any role the mesh does not define — the guard's allowlist is Viewer/Commenter and fail-closed). A data tree that is imported from a repo makes its partition SYSTEM-OWNED: AccessAssignmentGuard.IsForbiddenOnSystemOwned refuses every privileged grant in it, and SystemOwnedAccessRetractionHandler deletes any that predate the sync. Platform admin comes from Auth:GlobalAdmins in appsettings (GlobalAdminSeed writes the Admin/_Access grant at startup); per-space write is granted on the live mesh through the access UI. ShippedAccessGrantsTest enforces this over the whole repo.

Graph Storage Configuration

The monolith portal loads sample data from the filesystem. Paths are declared in appsettings.Development.json, relative to the monolith project directory:

{
  "Graph": {
    "Storage": {
      "Type": "FileSystem",
      "BasePath": "../samples/Graph/Data"
    }
  },
  "Storage": {
    "Name": "storage",
    "SourceType": "FileSystem",
    "BasePath": "../samples/Graph"
  }
}

Note the two sections are siblings: Graph:Storage is the node store, and the top-level Storage section is the content/blob store — there is no Graph:Content. The generator rewrites the BasePath values from ../../ (their depth inside memex/) to ../ when it emits the template.

The distributed portal uses PostgreSQL instead — no file paths required.

What the Framework Provides Out of the Box

AddGraph() and AddDocumentation(), called in the shared configuration, register built-in resources that are not part of the template's samples/ directory. You get these automatically:

Resource Details
Node types Markdown, Code, Agent, Group, User, VUser, Role, Notification, Approval, AccessAssignment, GroupMembership, and more
Documentation Architecture guides, DataMesh reference, GUI controls, AI integration docs (served under Doc/)
Icons Node type icons at /static/NodeTypeIcons/
Roles Built-in Admin, Editor, and Viewer role definitions

Customizing Your Portal

Adding Users

Create a JSON file in samples/Graph/Data/User/ and a matching access assignment in User/_Access/:

{
  "id": "Jane",
  "namespace": "User",
  "name": "Jane Doe",
  "nodeType": "User",
  "icon": "/static/NodeTypeIcons/person.svg",
  "isPersistent": true,
  "content": {
    "$type": "User",
    "email": "jane@example.com",
    "bio": "Product manager."
  }
}
{
  "id": "Jane_Access",
  "namespace": "User/_Access",
  "nodeType": "AccessAssignment",
  "content": {
    "$type": "AccessAssignment",
    "accessObject": "Jane",
    "displayName": "Jane Doe",
    "roles": [{ "role": "Admin" }]
  }
}

Adding a New Organization

Mirror the ACME structure under samples/Graph/Data/:

samples/Graph/Data/MyOrg/
├── MyOrg.json              # Organization root node
├── Project/                # Projects
├── User/                   # Org-scoped users
├── Doc/                    # Org documentation
└── _Access/                # Org-level access assignments

Replacing the Sample Data

Delete the ACME/ directory and add your own data. The portal loads whatever is in samples/Graph/Data/ — there are no hard-coded references to ACME anywhere in the framework.

Moving to Production Auth

🚨 DevLogin is not gated on the environment. It is enabled by the resolved authentication provider: Auth:EnableDevLogin when set, otherwise true whenever the provider resolves to Dev — which is the fallback when no external providers and no Entra ID configuration are present. So a portal deployed with ASPNETCORE_ENVIRONMENT=Production but no auth configured still serves the dev login page.

Before going to production, configure a real provider (Entra ID / an external OAuth provider) — and set Auth:EnableDevLogin=false explicitly if you want belt-and-braces. See Deployment for secrets management and redirect URI setup.

Monolith vs. Distributed

| Aspect | Monolith | Distributed (Aspire) | |--------|----------|---------------------| | Dependencies | None | Docker (PostgreSQL, Azurite) | | Data storage | Filesystem (samples/Graph/Data/) | PostgreSQL with pgvector | | Scaling | Single process | Orleans clustering, Azure Container Apps | | Primary use case | Local development, demos | Staging, production | | Run command | dotnet run --project MyProject.Portal.Monolith | dotnet run --project aspire/MyProject.AppHost | Monolith Distributed (Aspire) Portal.Monolith Portal.Shared samples/Graph/Data/ Filesystem In-memory AppHost (Aspire) Portal.Distributed Portal.Shared Orleans Silo PostgreSQL + pgvector Both modes share Portal.Shared — identical UI, auth, and business logic. Switch from monolith to distributed by changing the run command.

Start with the monolith during development — it has no external dependencies and restarts in seconds. When you need persistence, full-text search, vector search, or multi-instance scaling, switch to distributed mode. Both share the same MyProject.Portal.Shared project, so all UI, configuration, and business logic is identical across the two modes.

Troubleshooting

"Address already in use" on startup

The port the portal binds is occupied by another process. Either stop that process, or set the URLs explicitly (--urls "https://localhost:7123;http://localhost:5023", or in a Properties/launchSettings.json you add).

Dev login shows no users

The DevLogin page lists users via AccessSubjectQueries.Users. Make sure your user JSON files live in samples/Graph/Data/User/ and carry "nodeType": "User". If you have copied the query into your own code, check it is not the legacy namespace:User shape — that one returns zero rows.

"Access denied" after login

The user node exists but has no access assignment. Create an AccessAssignment node in User/_Access/ granting the user a role (Admin, Editor, or Viewer).

Portal crashes on startup (missing Graph:Storage)

ASPNETCORE_ENVIRONMENT is not set to Development, so appsettings.Development.json — which is where the storage paths live — is never layered in. The template ships no launchSettings.json, so pass --environment Development on the command line or add one that sets the variable.

ACME data not loading

Check that appsettings.Development.json has correct relative paths. From the monolith project directory, ../samples/Graph/Data should resolve to the samples/ folder at the solution root.

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.