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:
- A running portal with sample data (an ACME insurance company demo) from day one
- Dev login with pre-configured sample users (Alice, Bob, Carol, David, Emma, TestUser) for frictionless local development
- Two deployment modes — a lightweight monolith with no external dependencies, and a full distributed stack with Aspire, Orleans, and PostgreSQL
- Proper namespace renaming —
dotnet newreplaces everyMemexreference with your project name throughout the generated solution
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), sodotnet runuses the ASP.NET defaults unless you add one. SetASPNETCORE_ENVIRONMENT=Developmentyourself — either by adding alaunchSettings.jsonor 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 andACME/_Access/the partition-scoped ones — all of them read-only (Viewer).
🚨 A shipped
_Accessfile may never confer WRITE (Admin,Editor,PlatformAdmin, or any role the mesh does not define — the guard's allowlist isViewer/Commenterand fail-closed). A data tree that is imported from a repo makes its partition SYSTEM-OWNED:AccessAssignmentGuard.IsForbiddenOnSystemOwnedrefuses every privileged grant in it, andSystemOwnedAccessRetractionHandlerdeletes any that predate the sync. Platform admin comes fromAuth:GlobalAdminsinappsettings(GlobalAdminSeedwrites theAdmin/_Accessgrant at startup); per-space write is granted on the live mesh through the access UI.ShippedAccessGrantsTestenforces 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 |
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.