Charm 2.0 — a ground-up rewrite of the Charm Matrix client (matrix-rust-sdk over typed
Tauri IPC, new design language). This is the active charm project going forward.
Charm 1.0 (the matrix-js-sdk client) lives in
CloudHub-Social/Charm-1.0(GitHub:CloudHub-Social/Charm-1.0).
Charm 2.0 is under active pre-release development — expect breaking changes and missing features.
Charm's original source code and documentation are available under the
Apache License 2.0. Third-party components keep their own licenses;
see LICENSING.md and
THIRD_PARTY_NOTICES.md for scope and attribution;
distribution builds also embed a resolved THIRD_PARTY_LICENSES.txt inventory.
Sable Call is not included in Charm or covered by Charm's Apache license. The planned calling integration treats it as separately hosted external software and communicates with it only through the Matrix Widget API. See the licensing and external-widget boundary.
pnpm install
pnpm tauri dev # native desktop app
# or
pnpm dev # frontend only, in a browser (no Tauri IPC)Run pnpm lint, pnpm typecheck, pnpm test:coverage, and pnpm build before
opening a PR — see CONTRIBUTING.md for the full quality gate
and contribution guidelines.
Every night (and on demand via the Nightly builds
workflow), CI builds macOS/Windows/Linux/Android and publishes them to a
pre-release GitHub Release. Scheduled builds use the
date-only tag nightly-YYYY-MM-DD and overwrite that release if re-run the
same day. On-demand validation builds use the commit-qualified tag
nightly-YYYY-MM-DD-SHA, so their binaries and source archives stay aligned;
re-running the same commit updates that commit's release. These are
release-profile builds for testing — not signed by a trusted publisher, not
auto-updating, not for production use. iPhone and iPad builds are published through
the separate rolling ios-nightly prerelease so
AltStore and SideStore can keep a stable source URL. A paid Apple Developer Program
membership is not required: the device owner's Personal Team performs the final
signing. See the personal-device runbook.
Because the builds aren't signed by a certificate a trusted authority recognizes, each OS's normal "this isn't from a known publisher" gate needs a one-time bypass per download:
- macOS: the
.dmg/.appmay be signed with our self-signed cert (if configured — see below) or fully unsigned. Either way, macOS still blocks first launch as "from an unidentified developer." Right-click (or Control-click) the app → Open → Open in the confirmation dialog. A plain double-click will just refuse to launch. - Windows: launching the installer trips SmartScreen's "Windows protected your PC." Click More info, then Run anyway. This warning is reputation-based and persists even with our self-signed cert — only a paid EV/OV certificate with an established reputation removes it.
- Linux: install the
.deb/.rpmnormally (dpkg -i/rpm -ior your distro's package manager) — no publisher-trust gate to bypass. - Android: enable "Install unknown apps" for whatever app you used to
download the
.apk(Settings → Apps → Special access → Install unknown apps), then open the file. Signed with our persistent nightly keystore when configured (see below), which is what lets each night's APK install as an update over the previous one instead of requiring an uninstall first — Android refuses to install over an app it can't verify was signed by the same key. Without that keystore configured, it falls back to the Android Gradle Plugin's own auto-generated debug keystore, which is regenerated fresh on every CI run — every "nightly" would need a manual uninstall+reinstall in that case. - iOS / iPadOS: install AltStore or
SideStore, then add this source URL:
https://github.com/CloudHub-Social/Charm/releases/download/ios-nightly/altstore-source.json. The IPA is intentionally unsigned by Charm. The client re-signs it with your Personal Team and must refresh it about every seven days. This sideload lane deliberately excludes APNs and App Groups, so it supports foreground-resume sync and local notifications but not killed-state remote push.
Every asset attached to a nightly release (.dmg, .msi/.exe, .deb,
.rpm, .apk) can be checked two independent ways, both optional — neither
is enforced at install time the way the per-OS gates above are:
Checksums — SHA256SUMS.txt and SHA1SUMS.txt are attached to every
release, one line per artifact in standard sha256sum/sha1sum output
format. From the directory you downloaded into:
sha256sum -c SHA256SUMS.txt --ignore-missing # Linux
shasum -a 256 -c SHA256SUMS.txt --ignore-missing # macOS(--ignore-missing skips lines for platforms you didn't download; drop it
if you grabbed everything.) SHA1 is provided because it was asked for, not
because it adds any real security over SHA256 — SHA1 is broken for
collision resistance. Treat SHA256SUMS.txt and the GPG signatures below as
the actual integrity checks, and SHA1SUMS.txt as compatibility-only.
The iOS sideload prerelease instead ships an IPA-specific <filename>.ipa.sha256,
an SPDX SBOM, and a GitHub build provenance attestation. Verify the hash with
shasum -a 256 -c <filename>.ipa.sha256 before importing the source if you need an
independent download check. When its GPG signature is present, import the attached
versioned charm-ios-nightly-signing-key-<key-id>.asc; older retained iOS builds can
require an earlier attached key after a signing-key rotation.
GPG signatures — attached when GPG_PRIVATE_KEY is configured in the
protected nightly-signing or release-signing environment (see below): every artifact gets its own detached
<filename>.asc, and SHA256SUMS.txt/SHA1SUMS.txt are signed too (so
verifying SHA256SUMS.txt.asc alone vouches for every artifact's hash,
without checking each .asc individually — either approach works). The
public key ships as charm-nightly-signing-key.asc for nightlies and
charm-release-signing-key.asc for stable releases. Import the key attached
to the release you downloaded:
# Stable release:
gpg --import charm-release-signing-key.asc
# For a nightly, use: gpg --import charm-nightly-signing-key.asc
gpg --verify SHA256SUMS.txt.asc SHA256SUMS.txt
# or, for one specific artifact:
gpg --verify Charm_<version>_amd64.deb.asc Charm_<version>_amd64.debEach nightly and stable release also includes platform-named SPDX JSON
software bills of materials (charm-<platform>.spdx.json). Verify an SBOM
through the same signed checksum manifest before importing it into an
SPDX-compatible inventory or vulnerability scanner. It describes the
lockfile-pinned source commit used by that platform build. The scanner receives
a clean Git archive, not the post-build workspace or its runtime credentials,
caches, and signing material. This source inventory is not an exhaustive
inventory of packaged binaries or platform-resolved transitive dependencies.
This is a self-issued key, not backed by a CA or Apple/Microsoft's
notarization chains — it proves the file matches what this pipeline
produced, not that "this pipeline" is an identity you already trust from
anywhere else. Compare gpg's reported key fingerprint against the one
recorded when the key was generated (ask a maintainer) if you want that
assurance too.
Signing credentials must be stored only as environment secrets: nightly
identities in the protected nightly-signing environment and distinct stable
identities in the protected release-signing environment. Never store private
signing material or its passwords as repository secrets, where branch-selected
workflow YAML could access it. macOS/Windows/Android nightly builds are signed
automatically once the relevant nightly-signing secrets exist; until then,
each platform falls back to its previous unsigned/ephemeral-keystore behavior
(the workflow degrades gracefully either way). Generate and archive the
nightly and stable identities separately, then add each set of names below to
its matching environment. The example identity names are for nightly builds;
use clear Charm Release names for the stable set. All platforms' artifacts
are GPG-signed the same way (centrally, once every artifact has been built — see
nightly.yml's publish-nightly job), purely for download
provenance — none of the OS-level publisher-trust gates above are affected
by it, only whether a .asc signature is available to verify against.
macOS — Keychain Access → Certificate Assistant → Create a
Certificate… → Identity Type Self-Signed Root, Certificate Type
Code Signing (same flow as the local-dev cert in this repo's
CLAUDE.md, but exported instead of kept local). Then:
security export -k login.keychain-db -t identities -f pkcs12 -P "<a password>" -o cert.p12 \
-c "<the cert's common name>"
base64 -i cert.p12 -o cert.p12.b64If you script this with openssl pkcs12 -export instead of security export (e.g. to
generate a cert without ever touching a local Keychain), add -legacy. OpenSSL 3.x's
default PKCS12 encryption (AES-256/SHA-256) fails to import into macOS's Keychain with a
misleading MAC verification failed (wrong password?) error even when the password is
correct — confirmed the hard way in production. -legacy switches to the RC2/3DES +
SHA-1 encryption security import actually understands.
Add as environment secrets: MACOS_CERT_P12 (contents of cert.p12.b64),
MACOS_CERT_PASSWORD (the password used above), MACOS_CERT_NAME (the
cert's common name, exactly as it appears in Keychain Access).
For stable releases, use an Apple-issued Developer ID Application certificate
in release-signing, not the self-signed nightly identity. Also add APPLE_ID,
APPLE_PASSWORD (an app-specific password), and APPLE_TEAM_ID; Tauri uses
them to submit and staple the notarization ticket before the release workflow
accepts the app and disk image.
Windows nightly — use a self-signed test identity from PowerShell:
$cert = New-SelfSignedCertificate -Type CodeSigning -Subject "CN=Charm Nightly" `
-CertStoreLocation Cert:\CurrentUser\My -NotAfter (Get-Date).AddYears(5)
$password = ConvertTo-SecureString -String "<a password>" -Force -AsPlainText
Export-PfxCertificate -Cert $cert -FilePath cert.pfx -Password $password
[Convert]::ToBase64String([IO.File]::ReadAllBytes("cert.pfx")) | Out-File cert.pfx.b64Add as environment secrets: WINDOWS_CERT_PFX (contents of cert.pfx.b64),
WINDOWS_CERT_PASSWORD (the password used above) in nightly-signing.
Windows stable — obtain a long-lived Authenticode code-signing certificate
whose chain is trusted by Windows (for example, an OV/EV certificate issued by
a public CA), export that certificate and private key as a password-protected
PFX, and base64-encode the PFX. Store its encoded PFX and password under the
same secret names in release-signing. Do not copy the self-signed nightly
identity into that environment: stable CI requires Get-AuthenticodeSignature
to report Valid for the app and installers, and an untrusted self-signed
certificate cannot satisfy that gate. Certificate purchase, identity
validation, renewal, and archival are maintainer/provider operations; the
workflow never generates or replaces the stable identity.
The self-signed macOS and Windows identities described above are only suitable for nightly provenance. They do not remove the OS first-run friction described above and are not substitutes for the stable platform trust chains.
Android — a normal Java keystore via keytool (bundled with any JDK).
Unlike the macOS/Windows certs, this one's identity must stay stable
release over release — regenerating it later breaks in-place updates for
anyone who already installed a nightly, the same way losing it would:
keytool -genkeypair -v -keystore charm-nightly.keystore -alias charm-nightly \
-keyalg RSA -keysize 2048 -validity 10000 \
-storepass "<a password>" -keypass "<a password>" \
-dname "CN=Charm Nightly, O=CloudHub Social"
base64 -i charm-nightly.keystore -o charm-nightly.keystore.b64 # macOS
# base64 -w0 charm-nightly.keystore > charm-nightly.keystore.b64 # LinuxAdd as environment secrets: ANDROID_KEYSTORE_JKS (contents of
charm-nightly.keystore.b64), ANDROID_KEYSTORE_PASSWORD and
ANDROID_KEY_PASSWORD (the password used above — keytool above sets both
to the same value, but they can differ), ANDROID_KEY_ALIAS (charm-nightly
above). Back up charm-nightly.keystore and its passwords somewhere
durable (e.g. Bitwarden) before deleting the local copy — there's no
recovery path if it's lost, only starting over with a new identity that
breaks upgrades for existing installs.
Linux (GPG) — any GPG keypair; use a passphrase-protected one because the private key is still sensitive even inside a protected environment:
gpg --batch --full-generate-key <<'EOF'
%no-protection
Key-Type: RSA
Key-Length: 4096
Name-Real: Charm Nightly
Name-Email: nightly@cloudhub.social
Expire-Date: 2y
EOF(Use a real passphrase-protected key instead of %no-protection — swap in Passphrase: <a password> and drop %no-protection.) Then export both halves:
key_id=$(gpg --list-secret-keys --with-colons | awk -F: '/^sec/ { print $5; exit }')
gpg --export-secret-keys --armor "$key_id" > charm-nightly-gpg-private.ascAdd as environment secrets: GPG_PRIVATE_KEY (contents of
charm-nightly-gpg-private.asc), GPG_PASSPHRASE (empty string is fine if
you used %no-protection above). The public key is re-exported and
published as a release asset (charm-nightly-signing-key.asc) by the
workflow itself on every signed run, so there's nothing else to distribute
by hand.
The nightly workflow's Rust builds are cached in a shared DigitalOcean Spaces
(S3-compatible) bucket via sccache. SCCACHE_S3_ACCESS_KEY_ID /
SCCACHE_S3_SECRET_ACCESS_KEY are a read-write key, used only on main
(scheduled runs and main-branch dispatches) to populate the cache.
SCCACHE_S3_READONLY_ACCESS_KEY_ID / SCCACHE_S3_READONLY_SECRET_ACCESS_KEY
are an optional read-only key pair (create one scoped to read-only access on
the same Space) used for workflow_dispatch runs against any other branch,
so those still get cache hits without holding write credentials — this pair
is deliberately never used as a fallback on main itself, even if the
write-capable pair above is somehow missing, since sccache's own S3 startup
check requires write access and would otherwise fail with a confusing
permissions error instead of a clean "no cache configured". Every job also
sets SCCACHE_S3_RW_MODE=READ_ONLY whenever it's using this key pair (it
defaults to READ_WRITE regardless of which credentials are handed to it) —
without that, a cache miss on a read-only key still attempts a write and
gets AccessDenied instead of just skipping it. Neither secret
of a pair configured is fine too — every job falls back to a local-disk-only
cache (by leaving RUSTC_WRAPPER unset, so sccache is never invoked at all)
instead of hard-failing, rather than pointing sccache at the bucket with no
credentials (which used to abort the build with an S3 "InvalidArgument"
error).
This app publishes as plain Charm. Do not reintroduce a version suffix into any published-facing identifier:
package.jsonname:charm- Tauri
productName:Charm,identifier:social.cloudhub.charm - deep-link scheme:
charm:// - Cargo crate:
charm/charm_lib
No charm2, charm-2.0, Charm 2, or social.cloudhub.charm2 anywhere user- or
store-visible.
The real minisign keypair has been generated (pnpm tauri signer generate -w ~/.tauri/charm-updater.key) and the public half is in
src-tauri/tauri.conf.json's plugins.updater.pubkey. The private key stays local,
password-protected, never committed. Still TODO before shipping updates for real:
add endpoints once a release/update server exists.
Scope, architecture, design decisions, and feature specs live in
docs-site/src/content/docs/ and publish to
charm-docs.cloudhub.social.