Open-source business management platform evolving from inventory and procurement into a modular ERP.
Merconiq is an open-source business management platform built with .NET and PostgreSQL. It is evolving from a robust inventory and procurement foundation toward a modular ERP platform for small and growing businesses.
- Documentation Portal — the canonical documentation site on GitHub Pages.
- User Guide — for the people who will use the application day-to-day (login, items, stock operations, purchase orders, troubleshooting).
- README.md (this file) — for developers and operators: installation, architecture, API, deployment.
To publish GitHub Pages on a fork, enable Settings → Pages → Source: GitHub Actions and update
docs/_config.ymlwith the fork's project-site base path before verifying its URL.
| Layer | Technology |
|---|---|
| Runtime | .NET SDK 10.0.300, ASP.NET Core MVC |
| Database | PostgreSQL 16 + Entity Framework Core 10 |
| UI | MudBlazor 9 (responsive, no Bootstrap) |
| CQRS | MediatR 12 |
| Auth | ASP.NET Core Identity (RBAC) |
| Reports | QuestPDF |
| AI / ML | ML.NET — demand forecasting + anomaly detection |
| Logging | Serilog (console + rolling file) |
| API | RESTful with Asp.Versioning.Mvc (URL + header) |
| Containerization | Docker + Docker Compose |
| Testing | xUnit, Moq, FluentAssertions, AutoFixture, EF Core InMemory |
| CI/CD | GitHub Actions + GHCR |
The repository contains only the maintained web application and its supporting projects:
Merconiq.Core— domain entities, services, validators, and CQRS handlersMerconiq.Infrastructure— EF Core persistence and integrationsMerconiq.Web— ASP.NET Core API and Blazor UIMerconiq.Tests— unit and integration tests
The former Merconiq/ WinForms source tree was removed from the repository;
it is not part of the supported build or deployment path.
Inventory Management
- Full CRUD for items, suppliers, locations, and purchase orders
- Stock operations: receive, transfer between locations, sell
- Complete transaction history with filtering by date
Headless API
- Versioned RESTful API (
/api/v1/items,/api/v1/stock,/api/v1/forecast,/api/v1/anomalies) - MediatR-powered minimal endpoints
- URL segment and header-based versioning
AI-Powered Insights
- Demand forecasting per item using the configured implementation (managed moving average by default; ML.NET SSA is an explicit opt-in)
- Anomaly detection for unusual stock movements (spike/drop detection)
- Runs locally — zero cloud dependencies
Role-Based Access
- Admin, Manager, and Staff roles
- Secure login with ASP.NET Core Identity
Mobile-First UI
- MudBlazor component library for responsive design
- Works on desktop, tablet, and mobile browsers
PDF Reports
- QuestPDF for generating purchase orders and stock reports
Merconiq's planned platform domains include Accounting, CRM, Sales, Human Resources, Payroll, Manufacturing, Assets, Projects, Point of Sale, and Integrations. These are roadmap areas, not claims of currently implemented functionality.
git clone https://github.com/nirzaf/merconiq.git
cd merconiq
cp .env.example .env # edit credentials if desired
./scripts/validate-compose.sh development
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --waitThe app will be available at http://localhost:8080.
The development file is explicit: it enables Development mode, publishes the
local PostgreSQL port, mounts source directories, and enables the optional
watch configuration. Set DB_PASSWORD, JWT_SECRET, ADMIN_EMAIL, and
ADMIN_PASSWORD in .env when using the Development seed administrator;
validate-compose.sh validates required database/JWT values without printing
the resolved secrets.
Swagger UI is available at http://localhost:8080/swagger in the Development environment for interactive API exploration.
Set DB_PASSWORD and JWT_SECRET in .env before starting. There are no committed default credentials.
Prerequisites: .NET SDK 10.0.300 exactly (the repository pins this in global.json), PostgreSQL 16+, and Docker Desktop in Linux-container mode when running the PostgreSQL integration phase.
# 1. Create the database
createdb InventoryDB
# 2. Set the connection string and JWT secret through your shell environment
export ConnectionStrings__DefaultConnection="Host=localhost;Database=InventoryDB;Username=postgres;Password=$DB_PASSWORD"
export JwtSettings__Secret="$JWT_SECRET"
# 3. Apply migrations and start the development application
dotnet ef database update --project src/Merconiq.Infrastructure --startup-project src/Merconiq.Web
cd src/Merconiq.Web
dotnet runOpen the HTTP URL printed by dotnet run (this repository's launch profile uses http://localhost:5069; HTTPS is available only when the local development certificate is configured).
For local Development runs, configure the database connection and JWT secret with ASP.NET User Secrets instead of committing them:
dotnet user-secrets init --project src/Merconiq.Web
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Host=localhost;Database=InventoryDB;Username=postgres;Password=<local-password>" --project src/Merconiq.Web
dotnet user-secrets set "JwtSettings:Secret" "<at-least-32-byte-local-secret>" --project src/Merconiq.Web
dotnet user-secrets set "BootstrapAdmin:TenantId" "default" --project src/Merconiq.Web
dotnet user-secrets set "BootstrapAdmin:Email" "<admin-email>" --project src/Merconiq.Web
dotnet user-secrets set "BootstrapAdmin:Password" "<admin-password>" --project src/Merconiq.WebRun the one-shot bootstrap before the first login, then start the web application normally:
dotnet run --project src/Merconiq.Web -- --bootstrap-admin
dotnet run --project src/Merconiq.WebAll endpoints are prefixed with /api/v1.
| Method | Endpoint | Description |
|---|---|---|
GET |
/items |
List all items |
GET |
/items/{id} |
Get item by ID |
GET |
/stock |
List all stock in hand |
POST |
/stock/receive |
Receive stock |
GET |
/forecast/{itemId} |
Demand forecast for an item |
GET |
/forecast |
Forecast all items |
GET |
/anomalies |
Detect stock anomalies |
GET |
/companies/{companyId}/purchase-orders/{purchaseOrderId}/obligations |
Read company-authorized ordered charge-line quantities (View capability) |
POST |
/organization/companies/import |
Dry-run or apply tenant company master import (tenant Admin required) |
POST |
/organization/branches/import |
Dry-run or apply company-scoped branch import (Edit capability required) |
POST |
/organization/locations/import |
Dry-run or apply company-scoped location import (Administer capability required) |
POST |
/organization/suppliers/import |
Dry-run or apply company-scoped shared supplier import (Edit capability required) |
POST |
/organization/units/import |
Dry-run or apply company-scoped shared UOM import (tenant Admin and CompanyId required) |
POST |
/organization/items/import |
Dry-run or apply a company-scoped shared item master CSV import (Edit capability required) |
Master-data imports default to dry-run and use the existing bearer-token API. Every request
except company creation includes an explicit CompanyId; it is an authorization and onboarding
scope, not an ownership column for shared items, suppliers, or units. Every import requires stable
external_id values, reports row-level created, unchanged, or rejected results, and applies
no rows when any row is rejected. A valid apply can be replayed safely. CSV headers and synthetic
examples are documented in MASTER_DATA_ONBOARDING.md.
The purchase-order obligations view is read-only and reports ordered charge-line quantities only. It does not expose or infer receipt, acceptance, rejection, or outstanding quantities. Posted GoodsReceipt source-line progress, stock/GRNI posting, supplier returns, AP, and finance handoff are separate work.
src/Merconiq.Web/ # ASP.NET Core MVC + API
├── Controllers/ # MVC controllers (Items, Stock, Suppliers, etc.)
│ └── Api/V1/ # Versioned API controllers
├── Views/ # MudBlazor Razor views
├── Program.cs # App entry point + DI configuration
src/Merconiq.Core/ # Domain layer
├── Entities/ # Item, StockTransaction, Supplier, Location, etc.
├── Interfaces/ # IItemService, IStockService, IUnitOfWork, etc.
├── Services/ # Business logic + ML.NET AI services
├── Features/ # MediatR CQRS (Commands, Queries, Handlers)
└── Models/ # DTOs (DemandForecastResult, StockAnomaly)
src/Merconiq.Infrastructure/ # Data access
├── Data/ # DbContext, migrations, seed data
└── Repositories/ # Generic Repository<T> implementation
tests/Merconiq.Tests/ # xUnit test suite
├── Core/Services/ # Service unit tests
├── Core/Handlers/ # MediatR handler tests
├── Web/Controllers/ # Controller tests
└── Integration/ # Integration tests (WebApplicationFactory)
dotnet build # build solution
dotnet test # run all tests
dotnet ef migrations add MigrationName # add migration
--project src/Merconiq.Infrastructure
--startup-project src/Merconiq.WebServices that perform multiple persistence operations as one business action can use the
IUnitOfWork transaction boundary:
await unitOfWork.BeginTransactionAsync(cancellationToken);
try
{
// Add or update entities through the repositories.
await unitOfWork.CommitTransactionAsync(cancellationToken);
}
catch
{
await unitOfWork.RollbackTransactionAsync(cancellationToken);
throw;
}CommitTransactionAsync saves pending changes before committing and rolls back on failure.
The normal SaveChangesAsync path remains available for single-operation service methods.
The Docker Compose production path is deliberately explicit and does not load a
development override. It keeps PostgreSQL on the private Compose network (no
host port is published), preserves the pgdata and dataprotection volumes,
and requires non-empty database and JWT secrets:
cp .env.example .env
# Edit .env: DB_PASSWORD and JWT_SECRET.
./scripts/validate-compose.sh production
docker compose -f docker-compose.yml --profile migrations run --rm migrator
docker compose -f docker-compose.yml --profile bootstrap run --rm bootstrap-admin
docker compose -f docker-compose.yml up -d --waitBefore the bootstrap command, set BOOTSTRAP_ADMIN_TENANT, BOOTSTRAP_ADMIN_EMAIL,
and BOOTSTRAP_ADMIN_PASSWORD in the untracked .env file. The command is explicit,
tenant-bound, safe to repeat, and fails if Identity rejects the supplied credentials.
Normal production web startup does not create users, roles, sample locations, or other
sample data. The migrator applies the committed schema before the bootstrap command and
application start; never commit the administrator password or data-protection keys.
Keep the dataprotection volume across restarts so existing sessions and protected
values retain their documented behavior.
# Publish
dotnet publish -c Release -o ./publish
# Docker Compose (production; the script selects docker-compose.yml explicitly)
cp .env.example .env # set DB_PASSWORD, JWT_SECRET, and one-shot bootstrap settings
./scripts/deploy.sh --migrate
# Automated deployment script
./scripts/deploy.sh --build --migrateThe production deployment script applies committed EF migrations through a one-shot SDK migrator before starting the runtime container. The web process does not run migrations on production startup. The CI pipeline (.github/workflows/ci.yml) builds, tests, and pushes a Docker image to GitHub Container Registry on every push to master.
Every push and pull request to master runs an automated pipeline, and tagged releases publish Docker images to GitHub Container Registry and cut a GitHub Release.
| Workflow | File | Trigger | Purpose |
|---|---|---|---|
| CI | .github/workflows/ci.yml |
PR + push to master |
Restore → build → validate architecture, Compose, and release-workflow contracts → run xUnit tests with coverage → upload coverage-report artifact. |
| Docker | .github/workflows/docker.yml |
Push to master & v*.*.* tags; manual candidate validation |
Resolves and revalidates one exact commit, then builds the multi-arch image (linux/amd64, linux/arm64) → configured name ghcr.io/nirzaf/merconiq with commit-specific sha-<full-commit> and branch/semver tag aliases. Verify the package and digest before use; tags are pointers. Manual dry runs do not log in or push. |
| Pages validation | .github/workflows/pages-validation.yml |
Pull requests to master that change docs or Pages build files |
Builds the documentation with Jekyll and checks the rendered entry page, guide, theme stylesheet, and project base path without deploying. |
| GitHub Pages | .github/workflows/pages.yml |
Push to master when docs, the Pages workflow, or its artifact validator changes |
Builds the Markdown documentation with Jekyll, verifies the entry page, guide, theme stylesheet, and project base path, then deploys the rendered _site artifact. |
| Release | .github/workflows/release.yml |
Push of v*.*.* tag; manual existing-tag validation |
Revalidates the exact tag commit, waits for the commit-specific SHA image tag and semver image tag to exist, then cuts a GitHub Release. Use the verified manifest digest as the immutable image identity. Manual dry runs do not create a release. |
| Dependabot | .github/dependabot.yml |
Weekly (Mon) | Opens grouped PRs for NuGet, GitHub Actions, and Docker base-image updates. |
- Bump versions as needed and merge to
master. The CI and Docker workflows run. - When ready to release, create and push a semver tag:
git tag v1.2.3 git push origin v1.2.3
- The Release workflow revalidates the tag’s exact commit and verifies the Docker workflow’s commit-specific
sha-<full-commit>andv1.2.3image tags before creating a GitHub Release. The Docker workflow publishes the multi-arch image withsha-<full-commit>,v1.2.3,1.2, and1;latestis reserved formaster. Registry tags are pointers; retain the successful run's manifest digest for an immutable image reference.
To exercise either workflow without publication, use its manual dry_run input. A manual Docker candidate may be a branch or existing tag; a manual Release candidate must be an existing vMAJOR.MINOR.PATCH tag. Both reject malformed refs and stale remote candidates.
The Pages workflow builds the Markdown files in docs/ into a rendered Jekyll site and deploys _site when documentation changes reach master. On a fork, enable Settings → Pages → Source: GitHub Actions and configure that fork's own base path. The canonical project URL is https://nirzaf.github.io/merconiq/; see the Documentation Portal for guides and live verification.
Test coverage is collected via coverlet.collector and uploaded as a build artifact named coverage-report (Cobertura XML). Download it from the Actions run to inspect line/branch coverage locally or pipe it into a future Codecov integration.
All workflows use the default GITHUB_TOKEN and require no additional secrets.
For master: require PR + 1 approval, require status checks build-and-test and Docker, require linear history, and disallow force pushes.
Pull requests are welcome. See CONTRIBUTING.md for guidelines and CODE_OF_CONDUCT.md for community standards.
Merconiq-authored material is available under the MIT License; see LICENSE. Third-party packages and copied browser assets retain their own licenses and notices. Review third-party notices before redistributing a built artifact; the repository MIT license does not relicense those components.
Built on the shoulders of open source: .NET, PostgreSQL, MudBlazor, MediatR, QuestPDF, Serilog, ML.NET, xUnit, and many more.
For end-user documentation, see USER_GUIDE.md.