Credential Prototype Development#
Credential prototypes define reusable sets of configuration fields that applications can reference. They describe what credentials look like (e.g. “Fio Bank API token + account number”) without storing actual secret values. Applications declare which credential prototype they need, and administrators fill in the real values per company.
Schema Reference#
MultiFlexi uses JSON Schema version 0.2.0 to validate credential prototype definitions.
Schema Location:
https://raw.githubusercontent.com/VitexSoftware/php-vitexsoftware-multiflexi-core/refs/heads/main/schema/credential-prototype.json
Required Fields:
uuid: Unique identifier (UUID v4 format)code: Short alphanumeric identifier (2–64 chars,^[a-zA-Z0-9_-]+$)name: Human-readable name (string or localized object)version: Semantic version (e.g.1.0.0)
Optional Fields:
description: Description (string or localized object)homepage: Project homepage URLtags: Array of keyword strings for filtering and discoveryurl: Additional information URLlogo: Filename of the SVG logo (stored in/usr/share/multiflexi/images/)fields: Array of field definitions (see below)
Field Definitions#
Each entry in the fields array describes one configuration field the credential type provides.
Required field properties:
keyword: Environment variable name (uppercase,^[A-Z][A-Z0-9_]*$)type: One ofstring,password,email,url,integer,boolean,select,textarea,file
Optional field properties:
name: Human-readable label (string or localized object)description: Field description (string or localized object)hint: Placeholder text shown in the form inputdefault_value: Pre-filled default valuerequired: Boolean, whether the field must be filled (default:false)options: Array of{"value": "...", "label": "..."}objects (forselecttype)
Localization#
The name and description fields at both the prototype and field level support localization. Use either a plain string or an object with ISO 639-1 language codes as keys:
{
"name": {
"en": "Fio Bank",
"cs": "Fio Banka"
},
"description": {
"en": "Credential type for Fio Bank API integration",
"cs": "Typ přihlašovacích údajů pro integraci s Fio Bank API"
}
}
When a localized object is used, the English (en) value is stored as the default in the database. All language variants are stored in the credential_prototype_translations and credential_prototype_field_translations tables.
Minimal Example#
{
"$schema": "https://raw.githubusercontent.com/VitexSoftware/php-vitexsoftware-multiflexi-core/refs/heads/main/schema/credential-prototype.json",
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"code": "MyService",
"name": "My Service API",
"description": "Credentials for My Service REST API",
"version": "1.0.0",
"homepage": "https://github.com/example/my-service",
"tags": ["API", "REST", "my-service"],
"logo": "my-service.svg",
"fields": [
{
"keyword": "MY_SERVICE_URL",
"type": "url",
"name": "API Endpoint",
"description": "Base URL of the My Service API",
"hint": "https://api.my-service.com/v1",
"required": true
},
{
"keyword": "MY_SERVICE_TOKEN",
"type": "password",
"name": "API Token",
"description": "Authentication token for the API",
"required": true
}
]
}
Full Example with Localization#
{
"$schema": "https://raw.githubusercontent.com/VitexSoftware/php-vitexsoftware-multiflexi-core/refs/heads/main/schema/credential-prototype.json",
"uuid": "f79aaa38-2eaf-453a-beee-3a2afa1221d5",
"code": "FioBank",
"name": {
"en": "Fio Bank",
"cs": "Fio Banka"
},
"description": {
"en": "Fio Bank credential type for integration with Fio Bank API",
"cs": "Typ přihlašovacích údajů pro integraci s Fio Bank API"
},
"version": "1.0",
"logo": "Fio.svg",
"homepage": "https://www.fio.cz/",
"tags": ["Fio", "bank", "finance", "API"],
"fields": [
{
"keyword": "ACCOUNT_NUMBER",
"type": "string",
"name": {
"en": "Fio Bank Account Number",
"cs": "Číslo účtu Fio Banky"
},
"description": {
"en": "Number of the Fio Bank account",
"cs": "Číslo účtu ve Fio Bance"
},
"hint": "123456789/2010",
"required": false
},
{
"keyword": "FIO_TOKEN",
"type": "string",
"name": {
"en": "Fio Bank Token",
"cs": "Token Fio Banky"
},
"description": {
"en": "Token for accessing the Fio Bank API",
"cs": "Token pro přístup k Fio Bank API"
},
"required": true
}
]
}
File Naming Convention#
Credential prototype JSON files follow the naming pattern:
<lowercase-code>.credprototype.json
For example: fiobank.credprototype.json, mail.credprototype.json.
Place the file in the multiflexi/ directory of your project:
my-project/
├── multiflexi/
│ ├── my-app.multiflexi.app.json
│ ├── my-service.credprototype.json
│ └── my-service-logo.svg
├── src/
└── debian/
Logo Files#
Logo SVGs are shared across all MultiFlexi web frontends (multiflexi.eu, multiflexi-web5, multiflexi-web). They are served from a unified location:
Development: src/images/{uuid}.svg or src/images/{LogoFilename}
Production (Debian): /usr/share/multiflexi/images/
In your debian/*.install file, install the logo to the shared path:
multiflexi/*.svg usr/share/multiflexi/images/
Warning
Do not install logos to /usr/share/multiflexi-web/images/ — that path is reserved for web UI assets. All application and credential prototype logos go to /usr/share/multiflexi/images/.
Database Schema#
The credential prototype data is stored across four tables:
credential_prototype — main prototype record:
id,uuid,code,name,description,versionurl,homepage,tags,logocreated_at,updated_at
credential_prototype_field — field definitions per prototype:
credential_prototype_id,keyword,type,name,descriptionhint,default_value,required,options(JSON)
credential_prototype_translations — localized name/description per language:
credential_prototype_id,lang,name,description
credential_prototype_field_translations — localized field strings per language:
credential_prototype_field_id,lang,name,description,hint
CLI Commands#
Sync credential prototype JSON files from installed packages into the database:
sudo multiflexi credential-prototype:sync
This scans all *.credprototype.json files from installed package paths (/usr/lib/*/multiflexi/) and imports or updates them in the database. Run it after installing or upgrading packages that ship credential prototype definitions.
List all registered credential prototypes:
sudo multiflexi credential-prototype:list
Import and Export#
Programmatic import (PHP):
$prototype = new \MultiFlexi\CredentialProtoType();
$jsonData = json_decode(file_get_contents('myservice.credprototype.json'), true);
$prototype->importJson($jsonData);
Programmatic export (PHP):
$prototype = new \MultiFlexi\CredentialProtoType(42); // load by ID
$exported = $prototype->exportJson();
file_put_contents('export.json', json_encode($exported, JSON_PRETTY_PRINT));
The exportJson() method outputs localized fields as objects when translations exist, and includes homepage and tags when populated.
PHP Credential Prototype Classes#
In addition to JSON definitions, credential prototypes can be implemented as PHP classes extending \MultiFlexi\CredentialProtoType. This is useful when the credential type needs runtime logic (e.g. reading a .env file, querying a vault, or validating connections).
namespace MultiFlexi\CredentialProtoType;
class MyService extends \MultiFlexi\CredentialProtoType
implements \MultiFlexi\credentialTypeInterface
{
public static string $logo = 'my-service.svg';
public function __construct()
{
parent::__construct();
$apiUrl = new \MultiFlexi\ConfigField(
'MY_SERVICE_URL', 'url',
_('API Endpoint'), _('Base URL of the My Service API')
);
$apiUrl->setHint('https://api.example.com')->setRequired(true);
$token = new \MultiFlexi\ConfigField(
'MY_SERVICE_TOKEN', 'password',
_('API Token'), _('Authentication token')
);
$token->setRequired(true);
$this->configFieldsInternal->addField($apiUrl);
$this->configFieldsInternal->addField($token);
}
public function uuid(): string
{
return '550e8400-e29b-41d4-a716-446655440000';
}
public function name(): string
{
return _('My Service API');
}
public function description(): string
{
return _('Credentials for My Service REST API');
}
public function logo(): string
{
return self::$logo;
}
}
Even when using a PHP class, it is recommended to also ship a .credprototype.json file so the credential-prototype:sync CLI command can register the prototype’s homepage, tags, and localized strings.
Adding a Live Availability Check#
Implement \MultiFlexi\checkableCredentialInterface on your prototype class to provide
a runtime availability check. The base class \MultiFlexi\CredentialProtoType already
declares a default no-op implementation that returns CredentialState::Unknown (which
never blocks a job), so all existing prototypes remain backward compatible.
namespace MultiFlexi\CredentialProtoType;
class MyService extends \MultiFlexi\CredentialProtoType
implements \MultiFlexi\credentialTypeInterface,
\MultiFlexi\checkableCredentialInterface
{
#[\Override]
public function checkAvailability(): \MultiFlexi\CredentialCheckResult
{
$url = (string) ($this->configFieldsInternal->getFieldByCode('MY_URL')?->getValue() ?? '');
$token = (string) ($this->configFieldsInternal->getFieldByCode('MY_TOKEN')?->getValue() ?? '');
// 1. Offline validation — missing required fields
if ($url === '' || $token === '') {
return new \MultiFlexi\CredentialCheckResult(
\MultiFlexi\CredentialState::Misconfigured,
_('MY_URL or MY_TOKEN is not set'),
time(),
);
}
// 2. Live probe
$ch = curl_init($url.'/health');
curl_setopt_array($ch, [\CURLOPT_NOBODY => true, \CURLOPT_TIMEOUT => 5,
\CURLOPT_HTTPHEADER => ['Authorization: Bearer '.$token]]);
curl_exec($ch);
$errno = curl_errno($ch);
$http = (int) curl_getinfo($ch, \CURLINFO_HTTP_CODE);
if ($errno !== 0) {
return new \MultiFlexi\CredentialCheckResult(
\MultiFlexi\CredentialState::Unavailable,
sprintf(_('My Service unreachable: %s'), curl_strerror($errno)),
time(), 60,
);
}
if ($http === 401 || $http === 403) {
return new \MultiFlexi\CredentialCheckResult(
\MultiFlexi\CredentialState::Misconfigured,
sprintf(_('Authentication failed (HTTP %d)'), $http),
time(),
);
}
return new \MultiFlexi\CredentialCheckResult(
\MultiFlexi\CredentialState::Available, '', time(), 300
);
}
}
State guide:
State |
Retry? |
When to return |
|---|---|---|
|
— |
Service reachable and credential valid |
|
Yes (deferred) |
Reachable but impaired (busy, overloaded) |
|
Yes (deferred) |
Transient: timeout, connection refused, 5xx |
|
No |
Permanent: bad token, unreadable cert, missing field |
|
— |
Default (no check implemented) — never blocks |
The ttl parameter (seconds) tells the executor how long to cache the result.
Use shorter TTLs for transient failures (60 s) and longer for successes (300 s).
Rate-limited APIs should return a long TTL even on success to avoid burning quota.
Classifying TLS/Certificate Failures#
A plain $errno !== 0 check (as in the minimal example above) lumps every
transport failure — including an expired or untrusted server certificate —
into Unavailable. That is wrong for certificates: an expired cert will
never “become reachable” again on its own, so treating it as transient causes
the executor to retry forever instead of surfacing an actionable error.
\MultiFlexi\CredentialProtoType\AbraFlexi::checkAvailability() shows the
fix. It keeps a list of curl error codes that indicate a TLS/certificate
problem rather than a network hiccup:
private const TLS_ERRNOS = [
\CURLE_SSL_CONNECT_ERROR, // 35
51, // CURLE_PEER_FAILED_VERIFICATION / CURLE_SSL_PEER_CERTIFICATE
\CURLE_SSL_CERTPROBLEM, // 58 - local client cert problem
\CURLE_SSL_CIPHER, // 59
\CURLE_SSL_CACERT, // 60 - peer cert could not be verified against CA
\CURLE_SSL_CACERT_BADFILE, // 77
82, // CURLE_SSL_CRL_BADFILE
83, // CURLE_SSL_ISSUER_ERROR
];
(A few libcurl error codes are not exposed as PHP constants on every build — those are used as bare integers with a comment.)
When the initial probe fails with one of these codes, re-probe once with
CURLOPT_SSL_VERIFYPEER / CURLOPT_SSL_VERIFYHOST disabled and
CURLOPT_CERTINFO => true, then read CURLINFO_CERTINFO to get the
peer certificate’s Expire date. If that date is in the past, return
Misconfigured with the concrete expiry timestamp instead of curl’s
generic “SSL peer certificate … was not OK” text; otherwise fall back to a
Misconfigured “certificate is not trusted” message. Either way the result
is Misconfigured (no retry), because both cases need a human to fix the
server’s certificate, not the executor to keep polling it.
Packaging Checklist#
When creating a Debian package that includes a credential prototype:
Place the JSON definition in
multiflexi/<code>.credprototype.jsonPlace the logo SVG in
multiflexi/<code>.svgorsrc/images/<LogoName>.svgAdd to
debian/<package>.install:multiflexi/*.json usr/lib/<package>/multiflexi/ multiflexi/*.svg usr/share/multiflexi/images/
The
postinstscript should callmultiflexi credential-prototype:syncto register the prototype in the database after package installation.Include
homepageandtagsfields in the JSON for discoverability on multiflexi.eu.