Originally published on the PermitCore blog.
A license check at startup has two jobs: get the app on screen quickly, and decide which features the customer can use. Those jobs do not have to happen in the same moment. Render the first window from local proof, then confirm the entitlement with the server in the background and update the UI when the answer arrives.
1. Open the window and verify local proof
Show the main shell right away and keep license settings and the user's own files reachable. If your licensing policy allows offline use, verify the cached signed token against your trusted public key, and check that it belongs to the expected device and product. Check its expiry every time it is used.
- No token, or a token that fails verification: keep paid features restricted until the server confirms access.
- A revocation the app has already seen: persist it, so restarting with an old token on disk cannot bring access back.
In PermitCore, the offline flow accepts a server-issued signed token through ActivateOffline / activate_offline and checks it locally with ValidateOffline / validate_offline. Use the same trusted public key, device and product context at both steps. The offline licensing setup walks through the configuration.
2. Run the online check after the window renders
Run one validation per license at a time, with a short timeout. Retry transient failures with a small random delay, and respect the server's retry window when you are rate limited. Validation goes through the validate endpoint; device activation is its own step.
C#:
static async Task<LicenseResult> CheckAfterLaunchAsync( PermitCoreClient client, string key, Guid productId, CancellationToken windowClosing) { using var timeout = CancellationTokenSource.CreateLinkedTokenSource(windowClosing); timeout.CancelAfter(TimeSpan.FromSeconds(2)); return await client.ValidateAsync(key, expectedProductId: productId, ct: timeout.Token); } Python:
import asyncio async def check_after_launch(client, key, product_id): return await asyncio.to_thread( client.validate, key, expected_product_id=product_id ) Construct the Python client with timeout=2. If your desktop toolkit has its own worker mechanism instead of asyncio, run the call there and hand the result back to the UI thread. Either way, ignore results that arrive for a window that has closed or a key that has been replaced.
3. Treat valid, denied and unknown as three different outcomes
- Verified online: apply the entitlement for that product. When the cached signed token gets close to expiry, fetch a freshly issued one from your trusted issuer.
- Explicit denial (revoked, expired, wrong product): persist the decision for that key, device and product, invalidate the offline authorization, and keep the recovery screen available.
- Unknown (timeout, network failure, HTTP 503): the entitlement is unresolved. Keep exactly the access backed by current, verified local proof and its existing expiry. Authentication or response-format errors are something to diagnose; they should never create or extend offline access.
The API error reference lists which responses are authoritative denials. Show a quiet status line during outages and a specific recovery action for denials. If paid access expires while offline, keep the user's documents safe and let them reconnect or enter another key.
4. Test the transitions and measure first paint
In an integration fixture:
- Delay the validation response past the timeout.
- Return HTTP 503.
- Return an authoritative revocation.
The first two must keep an unexpired offline grant working and keep an expired or missing grant restricted. The revocation must stop paid access and still apply after a restart with the old token on disk. Repeat with a wrong product, a changed device, a corrupted token and a rolled-back clock; treat unexpected clock changes as a reason to verify online.
Measure two numbers separately: time to the first interactive window, and time to the online entitlement. Compare p95 cold starts under normal latency, offline, and with a slow backend. Network time should never show up in the first number.
For longer interruptions, see what happens if a license server goes down, and for wording recovery screens, the license error messages guide.
How does your app handle startup today: does it wait for the license server, or open first and check in the background?


