Creating API
1. Create Project
The reference API backend project is the model for this section.
▶️ (1) create a new ASP.NET Core web API project (no authentication) named Cadmus<PRJ>Api: select None for Authentication type, ensure that Enable container support and Use HTTPS is disabled (we’ll provide our own Docker files), ensure that Use controllers, Enable OpenAPI support, and Do not use top-level statements are checked.
Remember to disable HTTPS. In most API configurations HTTPS is managed by a reverse proxy, and this option is not required here in development.
▶️ (2) remove mock WeatherForecast.cs class and its corresponding WeatherForecastController.cs class from the Controllers folder.
▶️ (3) add NuGet packages, e.g. (paste these references in the project file and modify them as needed, using the NuGet package manager to update all the packages):
<ItemGroup>
<PackageReference Include="Cadmus.Api.Config" />
<PackageReference Include="Cadmus.Api.Controllers" />
<PackageReference Include="Cadmus.Api.Controllers.Export" />
<PackageReference Include="Cadmus.Api.Controllers.Import" />
<PackageReference Include="Cadmus.Api.Models" />
<PackageReference Include="Cadmus.Api.Services" />
<PackageReference Include="Cadmus.Graph" />
<PackageReference Include="Cadmus.Graph.Ef.PgSql" />
<PackageReference Include="Cadmus.Graph.Extras" />
<PackageReference Include="Cadmus.Seed" />
<!-- add/remove parts packages as needed -->
<PackageReference Include="Cadmus.Seed.Codicology.Parts" />
<PackageReference Include="Cadmus.Seed.Epigraphy.Parts" />
<PackageReference Include="Cadmus.Seed.General.Parts" />
<PackageReference Include="Cadmus.Seed.Philology.Parts" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="Polly" />
<PackageReference Include="Scalar.AspNetCore" />
<PackageReference Include="Serilog" />
<PackageReference Include="Serilog.AspNetCore" />
<PackageReference Include="Serilog.Exceptions" />
<PackageReference Include="Serilog.Extensions.Hosting" />
<PackageReference Include="Serilog.Sinks.Console" />
<PackageReference Include="Serilog.Sinks.File" />
<PackageReference Include="Serilog.Sinks.MongoDB" />
<PackageReference Include="Serilog.Sinks.Postgresql.Alternative" />
</ItemGroup>
You can remove the Serilog sinks you are not going to use, like e.g. the PostgreSQL one.
2. Add Settings
▶️ (1) Add settings to appsettings.json (replace __PRJ__ with your project’s name). Feel free to customize them as required.
⚠️ Please notice that all the sensitive data like users and passwords are there only for illustration purposes, and they will be overwritten by environment variables set in the host server.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"Default": "mongodb://localhost:27017/{0}",
"Auth": "Server=localhost;Database={0};User Id=postgres;Password=postgres;Include Error Detail=True",
"Index": "Server=localhost;Database={0};User Id=postgres;Password=postgres;Include Error Detail=True",
"MongoLog": "mongodb://localhost:27017/cadmus-__PRJ__-log",
"PostgresLog": "Server=localhost;Database=cadmus-__PRJ__-log;User Id=postgres;Password=postgres;Include Error Detail=True"
},
"DatabaseNames": {
"Auth": "cadmus-__PRJ__-auth",
"Data": "cadmus-__PRJ__"
},
"Serilog": {
"Using": [
"Serilog.Sinks.Console",
"Serilog.Sinks.File",
"Serilog.Sinks.MongoDB",
"Serilog.Sinks.Postgresql.Alternative"
],
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Information",
"System": "Warning"
}
}
},
"Auditing": {
"File": true,
"Mongo": true,
"Postgres": false,
"Console": true
},
"AllowedOrigins": ["http://localhost:4200"],
"RateLimit": {
"IsDisabled": true,
"PermitLimit": 100,
"QueueLimit": 0,
"TimeWindow": "00:01:00"
},
"ForwardedHeaders": {
"KnownProxies": [],
"KnownNetworks": [],
"TrustAllProxies": false,
"ForwardLimit": 1
},
"Seed": {
"ProfileSource": "%wwwroot%/seed-profile.json",
"ItemCount": 100,
"Delay": 0
},
"Jwt": {
"Issuer": "https://cadmus.azurewebsites.net",
"Audience": "https://www.fusisoft.it",
"SecureKey": "7W^3*y5@a!3%5Wu4xzd@au5Eh9mdFG6%WmzQpjDEB8#F5nXT"
},
"StockUsers": [
{
"UserName": "zeus",
"Password": "P4ss-W0rd!",
"Email": "dfusi@hotmail.com",
"Roles": ["admin", "editor", "operator", "visitor"],
"FirstName": "Daniele",
"LastName": "Fusi"
}
],
"Messaging": {
"AppName": "Cadmus __PRJ__",
"ApiRootUrl": "https://cadmus.azurewebsites.net/api/",
"AppRootUrl": "https://fusisoft.it/apps/cadmus/",
"SupportEmail": "webmaster@fusisoft.net"
},
"Editing": {
"BaseToLayerToleranceSeconds": 60
},
"Indexing": {
"IsEnabled": true,
"IsGraphEnabled": false
},
"Preview": {
"IsEnabled": true
},
"Mailer": {
"IsEnabled": false,
"SenderEmail": "webmaster@fusisoft.net",
"SenderName": "Cadmus __PRJ__",
"Host": "",
"Port": 0,
"UseSsl": true,
"UserName": "place in environment",
"Password": "place in environment"
}
}
⚠️ before API v10, the authentication database was MongoDB. Now it is a PostgreSQL database, as specified by
ConnectionStrings:AuthandDatabaseNames:Auth.
3. Add Program
▶️ (1) Use this template to replace the code in Program.cs (replace __PRJ__ with your project’s name):
using Cadmus.Api.Services;
using Cadmus.Api.Services.Seeding;
using Cadmus.Core;
using CadmusApi.Services;
using Fusi.Api.Auth.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Serilog;
using System.Diagnostics;
using Cadmus.Core.Config;
using Cadmus.Seed;
using Fusi.Api.Auth.Services;
using System.Text.Json;
using Serilog.Events;
using Microsoft.AspNetCore.HttpOverrides;
using Scalar.AspNetCore;
using Cadmus.Api.Controllers;
using Cadmus.Api.Controllers.Import;
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Configuration;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Cadmus.Api.Config.Services;
using Cadmus.Api.Config;
namespace Cadmus__PRJ__Api;
/// <summary>
/// Program.
/// </summary>
public static class Program
{
// startup log file name, Serilog is configured later via appsettings.json
private const string STARTUP_LOG_NAME = "startup.log";
private static void ConfigureAppServices(IServiceCollection services,
IConfiguration config)
{
// Cadmus repository
string dataCS = string.Format(
config.GetConnectionString("Default")!,
config.GetValue<string>("DatabaseNames:Data"));
services.AddSingleton<IRepositoryProvider>(
_ => new AppRepositoryProvider { ConnectionString = dataCS });
// part seeder factory provider
services.AddSingleton<IPartSeederFactoryProvider,
AppPartSeederFactoryProvider>();
// item browser factory provider
services.AddSingleton<IItemBrowserFactoryProvider>(_ =>
new StandardItemBrowserFactoryProvider(
config.GetConnectionString("Default")!));
// metadata builder factory provider
services.AddSingleton<IItemMetadataBuilderFactoryProvider>(_ =>
new StandardItemMetadataBuilderFactoryProvider(
config.GetConnectionString("Default")!));
// index and graph
ServiceConfigurator.ConfigureIndexServices(services, config);
ServiceConfigurator.ConfigureGraphServices(services, config);
// previewer
services.AddSingleton(p => ServiceConfigurator.GetPreviewer(p, config));
}
/// <summary>
/// Entry point.
/// </summary>
/// <param name="args">The arguments.</param>
public static async Task<int> Main(string[] args)
{
// early startup logging to ensure we catch any exceptions
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.Enrich.FromLogContext()
.WriteTo.Console()
#if DEBUG
.WriteTo.File(STARTUP_LOG_NAME, rollingInterval: RollingInterval.Day)
#endif
.CreateLogger();
try
{
Log.Information("Starting Cadmus API host");
ServiceConfigurator.DumpEnvironmentVars();
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
ServiceConfigurator.ConfigureLogger(builder);
IConfiguration config = new ConfigurationService(builder.Environment)
.Configuration;
ServiceConfigurator.ConfigureServices(builder.Services, config,
builder.Environment);
ConfigureAppServices(builder.Services, config);
builder.Services.AddOpenApi();
// controllers from Cadmus.Api.Controllers
builder.Services.AddControllers()
.AddApplicationPart(typeof(ItemController).Assembly)
.AddApplicationPart(typeof(ThesaurusImportController).Assembly)
.AddControllersAsServices();
WebApplication app = builder.Build();
// https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer
// this must come first, so that the client IP address (used by
// the rate limiter) and the original scheme (https) are restored
// before anything (e.g. OpenAPI servers URL) uses them.
// Only localhost is trusted by default: when behind a reverse
// proxy, list it in ForwardedHeaders:KnownProxies/KnownNetworks
ServiceConfigurator.UseForwardedHeaders(app, config);
// development or production
if (builder.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// https://docs.microsoft.com/en-us/aspnet/core/security/enforcing-ssl?view=aspnetcore-5.0&tabs=visual-studio
app.UseExceptionHandler("/Error");
if (config.GetValue<bool>("Server:UseHSTS"))
{
Console.WriteLine("HSTS: yes");
app.UseHsts();
}
else
{
Console.WriteLine("HSTS: no");
}
}
// HTTPS redirection
if (config.GetValue<bool>("Server:UseHttpsRedirection"))
{
Console.WriteLine("HttpsRedirection: yes");
app.UseHttpsRedirection();
}
else
{
Console.WriteLine("HttpsRedirection: no");
}
// CORS
app.UseCors("CorsPolicy");
// authentication
app.UseAuthentication();
// rate limiter (unless RateLimit:IsDisabled): after authentication,
// so that its partition key can be the authenticated user name
ServiceConfigurator.UseRateLimiter(app, config);
app.UseAuthorization();
// proxy
app.UseResponseCaching();
// seed auth database (via Services/HostAuthSeedExtensions)
await app.SeedAuthAsync();
// seed Cadmus database (via Services/HostSeedExtension)
await app.SeedAsync();
// map controllers and Scalar API
app.MapControllers();
app.MapOpenApi();
app.MapScalarApiReference(options =>
{
options.WithTitle("Cadmus API")
.AddPreferredSecuritySchemes("Bearer");
});
Log.Information("Running API");
await app.RunAsync();
return 0;
}
catch (Exception ex)
{
Log.Fatal(ex, "Cadmus API host terminated unexpectedly");
Debug.WriteLine(ex.ToString());
Console.WriteLine(ex.ToString());
return 1;
}
finally
{
await Log.CloseAndFlushAsync();
}
}
}
If you are not going to use a project-specific services library, add your app services in a new Services folder:
AppRepositoryProvider.cs: parts.AppPartSeederFactoryProvider.cs: part seeders.
💡 It is suggested to add a
Cadmus.__PRJ__.Servicesproject to contain these project-specific files if you are going to reuse them. This typically happens when you plan to use a CLI tool (either the generic tool – to build a plugin for it – or a project-specific tool).
Here are two example implementations; just customize the assemblies to include:
- 📁
AppRepositoryProvider.cs(optional):
using System;
using System.Reflection;
using Cadmus.Core;
using Cadmus.Core.Config;
using Cadmus.Core.Storage;
using Cadmus.Epigraphy.Parts;
using Cadmus.General.Parts;
using Cadmus.Geo.Parts;
using Cadmus.Mongo;
using Cadmus.Philology.Parts;
namespace CadmusDemoApi.Services;
/// <summary>
/// Application's repository provider. Usually, this is implemented in your
/// project's Services library. Here we have no specific project, so we
/// just provide an API app service here.
/// </summary>
public sealed class AppRepositoryProvider : IRepositoryProvider
{
private readonly IPartTypeProvider _partTypeProvider;
/// <summary>
/// Gets or sets the connection string.
/// </summary>
public string ConnectionString { get; set; } = "";
/// <summary>
/// Initializes a new instance of the <see cref="AppRepositoryProvider"/> class.
/// </summary>
/// <exception cref="ArgumentNullException">configuration</exception>
public AppRepositoryProvider()
{
TagAttributeToTypeMap _map = new();
_map.Add(
[
// TODO: add/remove assemblies as required for your app
// Cadmus.General.Parts
typeof(NotePart).GetTypeInfo().Assembly,
// Cadmus.Philology.Parts
typeof(ApparatusLayerFragment).GetTypeInfo().Assembly,
// Cadmus.Epigraphy.Parts
typeof(EpiScriptsPart).GetTypeInfo().Assembly,
// Cadmus.Geo.Parts
typeof(AssertedLocationsPart).GetTypeInfo().Assembly
]);
_partTypeProvider = new StandardPartTypeProvider(_map);
}
/// <summary>
/// Gets the part type provider.
/// </summary>
/// <returns>part type provider</returns>
public IPartTypeProvider GetPartTypeProvider()
{
return _partTypeProvider;
}
/// <summary>
/// Creates a Cadmus repository.
/// </summary>
/// <returns>repository</returns>
/// <exception cref="ArgumentNullException">null database</exception>
public ICadmusRepository CreateRepository()
{
// create the repository (no need to use container here)
MongoCadmusRepository repository =
new(_partTypeProvider, new StandardItemSortKeyBuilder());
repository.Configure(new MongoCadmusRepositoryOptions
{
ConnectionString = ConnectionString ??
throw new InvalidOperationException(
"No connection string set for IRepositoryProvider implementation")
});
return repository;
}
}
- 📁
AppPartSeederFactoryProvider.cs(optional):
using Cadmus.Core.Config;
using Cadmus.Seed;
using Cadmus.Seed.Epigraphy.Parts;
using Cadmus.Seed.General.Parts;
using Cadmus.Seed.Geo.Parts;
using Cadmus.Seed.Philology.Parts;
using Fusi.Microsoft.Extensions.Configuration.InMemoryJson;
using Microsoft.Extensions.Hosting;
using System;
using System.Reflection;
namespace CadmusDemoApi.Services;
/// <summary>
/// Application's part seeders factory provider. Usually, this is implemented
/// in your project's Services library. Here we have no specific project, so we
/// just provide an API app service here.
/// </summary>
public sealed class AppPartSeederFactoryProvider : IPartSeederFactoryProvider
{
private static IHost GetHost(string config)
{
// build the tags to types map for parts/fragments
Assembly[] seedAssemblies =
[
// TODO: add/remove assemblies as required for your app
// Cadmus.General.Seed.Parts
typeof(NotePartSeeder).Assembly,
// Cadmus.Seed.Philology.Parts
typeof(ApparatusLayerFragmentSeeder).Assembly,
// Cadmus.Seed.Geo.Parts
typeof(AssertedLocationsPartSeeder).GetTypeInfo().Assembly,
// Cadmus.Seed.Epigraphy.Parts
typeof(EpiScriptsPartSeeder).GetTypeInfo().Assembly
];
TagAttributeToTypeMap map = new();
map.Add(seedAssemblies);
return new HostBuilder()
.ConfigureServices((hostContext, services) =>
{
PartSeederFactory.ConfigureServices(services,
new StandardPartTypeProvider(map),
seedAssemblies);
})
// extension method from Fusi library
.AddInMemoryJson(config)
.Build();
}
/// <summary>
/// Gets the part/fragment seeders factory.
/// </summary>
/// <param name="profile">The profile.</param>
/// <returns>Factory.</returns>
/// <exception cref="ArgumentNullException">profile</exception>
public PartSeederFactory GetFactory(string profile)
{
ArgumentNullException.ThrowIfNull(profile);
return new PartSeederFactory(GetHost(profile));
}
}
4. Add Assets
▶️ (1) Copy the whole wwwroot from CadmusApi, and customize its contents (the Cadmus profile, and if needed the messages template text).
Inside that folder, edit:
- the
seed-profile.jsonfile, which contains the full Cadmus configuration for data and editors used in the project:- remove all the parts, fragments, and seeders you do not use and all the parts, fragment, and seeders you require;
- do the same for thesaurus entries.
- the
preview-profile.jsonfile, which contains the configuration for parts preview. If you have no preview, just use an empty JSON object{}as its content.
This is the core customization for the whole project. Usually, the profile file is created after the documentation is completed, and before creating the code.
Inside the
messagesfolder you can customize the message templates as you prefer, but usually this is not required.
5. Setup Docker
▶️ (1) In the project’s root (where the .slnx file is located), add a Dockerfile to build the Docker image (replace __PRJ__ with your project’s name):
# Stage 1: base (uses target platform architecture for ASP.NET runtime)
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
EXPOSE 8080
EXPOSE 443
# Stage 2: build/publish (SDK runs natively on host platform for speed)
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG TARGETARCH
ARG TARGETOS
WORKDIR /src
# Copy project files and source
COPY . .
# Use bash to map amd64 -> x64 and build for the target RID cleanly
RUN /bin/bash -c '\
RID_ARCH="${TARGETARCH/amd64/x64}" && \
dotnet publish "Cadmus.__PRJ__.Api/Cadmus.__PRJ__.Api.csproj" \
-c Release \
-r "${TARGETOS:-linux}-${RID_ARCH}" \
--no-self-contained \
-o /app/publish'
# Stage 3: final image
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "Cadmus.__PRJ__.Api.dll"]
▶️ (2) add a docker-compose.yml file to allow you using the API in a composer stack (replace PRJ with your project name; of course, you can change your image name as required to fit your organization).
⚠️ ATTENTION: under cadmus-api ports replace 5052 with the port value used by your API project (you can find it under the project’s properties, Debug, Launch Profiles, HTTP).
services:
# MongoDB
cadmus-PRJ-mongo:
image: mongo
container_name: cadmus-PRJ-mongo
environment:
- MONGO_DATA_DIR=/data/db
- MONGO_LOG_DIR=/dev/null
command: mongod --logpath=/dev/null
ports:
- 27017:27017
networks:
- cadmus-PRJ-network
# PostgreSQL
cadmus-PRJ-pgsql:
image: postgres
container_name: cadmus-PRJ-pgsql
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=postgres
ports:
- 5432:5432
networks:
- cadmus-PRJ-network
# Biblio API
# TODO: remove if you are not using it
cadmus-biblio-api:
image: vedph2020/cadmus-biblio-api:7.0.0
container_name: cadmus-biblio-api
ports:
- 60058:8080
depends_on:
- cadmus-PRJ-mongo
- cadmus-PRJ-pgsql
environment:
- ASPNETCORE_URLS=http://+:8080
- CONNECTIONSTRINGS__DEFAULT=mongodb://cadmus-PRJ-mongo:27017/{0}
- CONNECTIONSTRINGS__AUTH=Server=cadmus-PRJ-pgsql;port=5432;Database={0};User Id=postgres;Password=postgres;Include Error Detail=True
- CONNECTIONSTRINGS__BIBLIO=Server=cadmus-PRJ-pgsql;port=5432;Database={0};User Id=postgres;Password=postgres;Include Error Detail=True
- SEED__BIBLIODELAY=50
- SERILOG__CONNECTIONSTRING=mongodb://cadmus-PRJ-mongo:27017/{0}-log
- STOCKUSERS__0__PASSWORD=P4ss-W0rd!
networks:
- cadmus-PRJ-network
# Cadmus PRJ API
cadmus-PRJ-api:
image: vedph2020/cadmus-PRJ-api:0.0.1
container_name: cadmus-PRJ-api
ports:
# TODO: set your port replacing 5052
- 5052:8080
depends_on:
- cadmus-PRJ-mongo
- cadmus-PRJ-pgsql
environment:
- ASPNETCORE_URLS=http://+:8080
- CONNECTIONSTRINGS__DEFAULT=mongodb://cadmus-PRJ-mongo:27017/{0}
- CONNECTIONSTRINGS__AUTH=Server=cadmus-PRJ-pgsql;port=5432;Database={0};User Id=postgres;Password=postgres;Include Error Detail=True
- CONNECTIONSTRINGS__INDEX=Server=cadmus-PRJ-pgsql;port=5432;Database={0};User Id=postgres;Password=postgres;Include Error Detail=True
- SERILOG__CONNECTIONSTRING=mongodb://cadmus-PRJ-mongo:27017/{0}-log
- STOCKUSERS__0__PASSWORD=P4ss-W0rd!
- SEED__DELAY=20
- MESSAGING__APIROOTURL=http://cadmusapi.azurewebsites.net
- MESSAGING__APPROOTURL=http://cadmusapi.com/
- MESSAGING__SUPPORTEMAIL=support@cadmus.com
networks:
- cadmus-PRJ-network
networks:
cadmus-PRJ-network:
driver: bridge
⚠️ Note that setting
ASPNETCORE_URLSfor Docker is a requirement because the default HTTP port for ASP.NET core in development mode is 5000.
▶️ (3) add a .dockerignore file with this content:
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
🐋 To build a Docker image:
(1) Before creating Docker images, ensure that you have published all the required NuGet packages and that you have a buildx builder instance running that supports multi-arch:
docker buildx create --use --name multi-arch-builder || docker buildx use multi-arch-builder
docker buildx inspect --bootstrap
To run natively on Linux VMs, macOS (both Intel and Apple Silicon), and Windows (via WSL2 or Docker Desktop)—
linux/amd64andlinux/arm64are the only two targets we need. Note thatdocker buildxautomatically injects variables likeTARGETARCHandTARGETOSinto the scope of your build. InDockerfilewe pass these directly to the .NET CLI commands.
(2) Build for multiple platforms and push directly to Docker Hub:
docker buildx build --platform linux/amd64,linux/arm64 -t vedph2020/cadmus-__PRJ__-api:0.0.1 -t vedph2020/cadmus-__PRJ__-api:latest --push .
When there is no available ARM image, add
platform: linux/amd64as a sibling of theimageline in docker compose script to force Docker to pull and run the x64 image under emulation, e.g.:
- add
platform: linux/arm64to MongoDB and PostgreSQL. Note that some images (especially for PostgreSQL) are not available for ARM, so you may need to addplatform: linux/amd64to them too or downgrade to a version which supports ARM (e.g.image: postgres:16). - add
platform: linux/amd64to API and app.
6. Add Readme
▶️ (1) Add a readme like this:
# Cadmus PRJ API
🐋 Quick Docker image build:
(1) ensure buildx is running:
```sh
docker buildx create --use --name multi-arch-builder || docker buildx use multi-arch-builder
docker buildx inspect --bootstrap
``
(2) build for multiple platforms and push directly to Docker Hub:
```sh
docker buildx build --platform linux/amd64,linux/arm64 -t vedph2020/cadmus-__PRJ__-api:0.0.1 -t vedph2020/cadmus-__PRJ__-api:latest --push .
``
(replace with the current version).
This is a Cadmus API layer customized for the PRJ project. Most of its code is derived from [shared Cadmus libraries](https://github.com/vedph/cadmus-api).