Few calls.
Any platform.

Identity Layer integrates with minimal surface by design. MicroPython on a microcontroller, Flutter on Android and Windows, or a PHP bridge dropped into your existing endpoints. The same protocol. The same cryptographic guarantees. Four calls.

Boot to verified. 6.5 seconds.

The same Ed25519 challenge/response protocol that authenticates a mobile user or a regulated operator runs on a microcontroller. Device-bound key. No credential database. JWT issued on verification.

Raspberry Pi Pico 2W, PN532 NFC reader, SSD1306 OLED display, NTAG424 DNA card
Boot
Registering
Node exists
Verified
Challenge
Active
Four operative calls. WiFi, OLED, and config loading are device infrastructure, not Identity Layer. The identity primitive is IdentityIoT, installed via mip.
MicroPython
# Install via mip — no external dependencies
# import mip; mip.install("github:wide/identity-micropython")

from identity_iot import IdentityIoT

# 1. Init — configure node identity
identity = IdentityIoT(
    base_url  = cfg['base_url'],
    node_type = cfg['node_type'],
    tenant_id = cfg['tenant_id'],
    comm_key  = cfg['comm_key'].encode(),
    comm_iv   = cfg['comm_iv'].encode(),
)

# 2. Register — device-bound key, runs once
identity.ensure_registered()

# 3. Authenticate — Ed25519 challenge / verify / JWT
token = identity.authenticate()

# 4. Authenticated request — AES-256-GCM encrypted payload
response = identity.post_encrypted({'request': 'record_sensor_data', 'value': 42})
Component Model Role
MCU Raspberry Pi Pico 2W (RP2350) TrustZone, WiFi, device-bound key storage
NFC reader PN532 I2C interface, reads NTAG424 DNA badges
Display SSD1306 128x64 OLED Boot sequence, verification status
NFC badge NTAG424 DNA (NXP) AES-128 onboard, non-exportable keys, dynamic CMAC
ESP32 and RP2040 reference deployments for datacenter and physical access verticals are in progress. The protocol and SDK are identical across all supported hardware.

Identity IoT nodes are not limited to NFC access. The same authenticated node can carry any sensor payload. All readings are transmitted via AES-256-GCM encrypted POST after the node obtains its JWT.

Sensor typeTypical hardwarePayload field
TemperatureDHT22, DS18B20, BME280temperature (float, °C)
HumidityDHT22, BME280humidity (float, %)
PressureBME280, BMP390pressure (float, hPa)
PIR motionHC-SR501motion (bool)
GPSNEO-6M, L76Xlat, lon, altitude (float)
Energy meteringPZEM-004T, INA219voltage, current, power (float)
NFC accessPN532 + NTAG424 DNAbadge_uid, granted (bool)
JSON (decrypted)
{
  "node_uid"   : "273ec83a-84c8-4967-aafa-0780ab161cc1",
  "tenant_id"  : "0b3cfe4d-fdf7-46c2-bd41-b533da415dd9",
  "node_type"  : "sensor",
  "temperature": 24.3,
  "humidity"   : 58.1,
  "pressure"   : 1013.2,
  "updated_at" : "2026-06-09 11:42:07"
}

Badge read. Challenge issued.
Ed25519 verified.

Identity Tag is the application layer built on top of Identity IoT. Each badge read triggers a full Ed25519 challenge/response cycle against the backend. No stored secrets on the reader. No cleartext identity on the badge. Multi-tenant by design.

Badge to GRANTED in four steps. The reader (Pico 2W + PN532) is an authenticated Identity IoT node. The badge carries an AES-CBC encrypted private key fragment and a split DEK. The backend issues a challenge, receives an Ed25519 signature, verifies it, and returns GRANTED or DENIED. No secrets traverse the wire in cleartext. No credential database exists.
Flow: badge read to access decision
# 1. Reader reads NTAG424 DNA NDEF payload
#    Fields: epk (encrypted private key), df2 (DEK fragment 2), mode (STD|HS)

# 2. DEK reconstruction
dek = df1_from_config XOR df2_from_badge

# 3. Decrypt badge private key
badge_privkey = AES_CBC_decrypt(dek, epk)

# 4. Request challenge from backend
challenge = POST identity_tag/it_request_challenge
#    { node_uid, badge_uid, tenant_id }

# 5. Sign challenge with badge key (Ed25519)
signature = ed25519_sign(badge_privkey, challenge_nonce)

# 6. Verify at backend
result = POST identity_tag/it_verify_badge
#    { challenge_id, signature_b64 }  ->  { granted: true }

# 7. Reader actuates relay (GRANTED) or displays DENIED
ModeDescriptionUse case
STD Badge works on any terminal in the authorised fleet for that tenant. Office access, general entry points.
HS Badge cryptographically bound to a specific terminal. The DEK derivation incorporates a hash of the terminal public key. Badge is invalid on any other reader. High-security rooms, datacenter racks, equipment cabinets.
Each tenant has an isolated namespace: its own node fleet, badge registry, group tree, and entitlement table. A badge registered to Tenant A cannot be verified by a node belonging to Tenant B. Tenant isolation is enforced at the database query level, not via application logic alone.
EndpointMethodRole
identity_tag/it_request_challengePOSTIssue nonce for badge verification
identity_tag/it_verify_badgePOSTVerify Ed25519 signature, return GRANTED/DENIED
identity_tag/it_node_checkinPOSTNode heartbeat and JWT refresh
identity_tag/it_register_badgePOST (manager)Write badge public key and entitlement
identity_tag/it_list_nodesGET (manager)Node fleet status for tenant

The provisioning and management app runs on Android. It uses the device's NFC to write badge payloads and connects to the backend over authenticated Identity Layer channels.

Dart / Flutter: badge write
// identity_tag package - badge provisioning
final writer = IdentityTagWriter(api: tagApi);

// Generate badge keypair and encrypt private key with split DEK
final payload = await writer.prepareBadgePayload(
  tenantId  : tenant.id,
  mode      : BadgeMode.STD,   // or BadgeMode.HS
  nodeUid   : null,              // required for HS mode
);

// Write NDEF to NTAG424 DNA via flutter_nfc_kit
await writer.writeToTag(payload);

Device-bound identity.
Four calls.

Local packages. No external identity service. The private key never leaves the device. Secure storage via flutter_secure_storage and TEE / Secure Enclave when available. Payload encryption is end-to-end via ITEMSEncrypter.

identity_package identity_std identity_hs items_crypto items_search items_document_body
All packages are local. No pub.dev dependencies on the identity layer. SDK is distributed as part of the licensing package.
Dart / Flutter
// 1. Init — JWT identity, silent on subsequent boots
final auth = IdentityAuthServiceStd(ITEMSGlobals.authURI);
await auth.ensureJwt(
  preferredLocale: locale.languageCode,
  utcOffsetMin:    DateTime.now().timeZoneOffset.inMinutes,
);

// 2. Handshake — silent if identity already present on device
final result = await api.mjHandshake(
  identityHash: identityHash,
  publicKey:    publicKey,
);

// 3. Store JWT — session active, no password, no credential database
await _storage.write(key: 'token', value: result['token']);

// 4. Every request — AES-256-GCM payload, end-to-end encrypted
final response = await api.postEncrypted({'request': 'your_request'});
YAML
dependencies:

  # Identity Layer — local packages, no external registry
  identity_package:
    path: packages/identity_package
  identity_std:
    path: packages/identity_std
  identity_hs:
    path: packages/identity_hs
  items_crypto:
    path: packages/items_crypto
PlatformStatusSecure storage
AndroidProduction, Google Play clearedAndroid Keystore / TEE
WindowsProduction, Microsoft Store clearedWindows Credential Manager
LinuxProductionlibsecret
iOS / macOSOn roadmapSecure Enclave

5 KB.
Your stack, unchanged.

The PHP bridge is a single require that sits in front of your existing endpoints. Your routes, your database, your business logic. Untouched.. Identity Layer handles authentication and payload encryption. Remove it the same way you added it.

Drop-in, drop-out. One require_once and two function calls. Your endpoints start receiving verified, decrypted payloads. No schema migration. No user table changes. No session storage. The complete server-side stack is 225 KB.
PHP
// 1. Include the Identity Layer bridge — 5 KB
require_once 'identity/_mj_auth.php';

// 2. Authenticate — JWT verified, identity_hash extracted
//    identity_hash is a 64-char SHA-256 hex — never a user ID
[$identityHash, $tenantId, $claims, $role] =
    mj_authenticate_identity($encoder);

// 3. Every response — AES-256-GCM encrypted, end-to-end
sendEncryptedResponse($encoder, [
    'status' => 'ok',
    'data'   => $yourData,
], ['ts' => time()], 200);
Audit logs exist but are cryptographically inaccessible. Access requires a validated court order. The mj_mandate_log endpoint registers the mandate and opens a 72-hour export window. Logs are destroyed after collection. The requesting authority bears legal responsibility.
PHP: mj_mandate_log
// Requires role = admin — never operator
[$userId, $tenantId, $claims, $role] = mj_authenticate($encoder);
mj_require_role($encoder, $claims, 'admin');

// Register mandate — opens 72h export window
// target_identity_hash: 64-char SHA-256, never plaintext identity
// Ciphertext is never stored — metadata only
$connector->executeDatabaseParameterQuery(
    "INSERT INTO audit_log (mandate_number, issuing_country,
     issuing_court, target_identity_hash, export_status)
     VALUES (?, ?, ?, ?, 'PENDING')",
    [$mandateNumber, $issuingCountry, $issuingCourt, $targetHash],
    $db
);
ComponentTechnologyNotes
RuntimePHP 8.xVanilla, no framework required
DatabaseMySQL / MariaDBStandard schema, no credential columns
Cache / ephemeralRedisChallenge tokens, message queues (TTL < 1 min)
Bridge size5 KBSingle PHP file, drop into any LAMP stack
Full stack size225 KBIdentity STD + HS + Keys + Secure Channel + IoT + Labels + Tag
DeploymentContainer / on-premise / air-gappedNo mandatory cloud connectivity