Skip to content

feat(cli): init apns and init push, with one-click APNs key creation - #1963

Closed
ArnabChatterjee20k wants to merge 36 commits into
messaging-provider-clifrom
messaging-provider-cli-apns
Closed

ArnabChatterjee20k wants to merge 36 commits into
messaging-provider-clifrom
messaging-provider-cli-apns

Conversation

@ArnabChatterjee20k

@ArnabChatterjee20k ArnabChatterjee20k commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Stacked on #1956 (init fcm). Merge that first.

Sets up Appwrite Push for iOS from the CLI: the APNs providers, the Xcode project, and, if wanted, the APNs key itself, created by signing in with an Apple ID. init push sets up iOS and Android together.

What's in this PR

appwrite init apns Creates or updates the APNs production and sandbox providers for an iOS app, and adds the aps-environment entitlement and the remote-notification background mode to its Xcode target.
appwrite init push Finds the iOS and Android apps in the project and runs init apns and/or init fcm (#1956) for each.
--create-key Creates the APNs key by signing in with the Apple ID (password, two-factor), with no trip to the Apple Developer portal. Experimental.
Setups Three interchangeable ways to create the key: appwrite (native Go, the default), expo (@expo/apple-utils) and fastlane (spaceship). Picked with --provider or APPWRITE_APNS_SETUP, so if Apple's private API breaks one, users switch instead of waiting for a release.

Merged into this branch

PR What
#1992 The internal/apns adapter contract and registry. --create-key, --provider / APPWRITE_APNS_SETUP, --environment, APPWRITE_APPLE_ID / APPWRITE_APPLE_PASSWORD. Saves the new .p8 before the providers are written. Stops when --create-key fails.
#1993 appwrite setup: Apple sign-in and Developer portal calls in Go (SRP-6a, hashcash, two-factor, a saved and trusted session). Nothing to install.
#1994 expo setup: @expo/apple-utils 2.2.1 through an embedded Node helper, pinned by lockfile hash.
#1995 fastlane setup: spaceship through the installed fastlane, or the project's Bundler fastlane when its Gemfile pins one.
#1997 expo downloads Node.js (LTS v24.21.0, SHA-256 pinned) when it is missing or older than 18.
#1998 fastlane offers brew install fastlane when fastlane is missing, and otherwise prints install or upgrade commands.

Fixes from testing against a real Apple account and from review, committed directly here:

  • The team list is sent without a body, and Developer portal calls are form encoded (JSON only to create a key). Apple answered both with HTTP 415.
  • Apple's current "maximum allowed number of team scoped Keys" message is recognised as a full team.
  • The hashcash difficulty is capped at 24 bits and the search can be cancelled.

Open on top of this PR

Flags

appwrite init apns

Flag Meaning When omitted
--bundle-id <id> iOS bundle ID PRODUCT_BUNDLE_IDENTIFIER of the app target, or Expo ios.bundleIdentifier. With several apps, a picker. With none, asked.
--team-id <id> 10-character Apple Developer team ID DEVELOPMENT_TEAM of the target, Expo ios.appleTeamId, or the team on an existing provider. With --create-key, the Apple sign-in picks it (asks when the account has several). Otherwise asked.
--key-path <file> An existing APNs auth key (.p8) A valid .p8 in the project folder. Otherwise asked: sign in with the Apple ID and create one, create it in the browser, or use a file.
--key-id <id> 10-character key ID From the AuthKey_<KEYID>.p8 file name, otherwise asked. Ignored for a created key, which uses Apple's ID.
--create-key Create the key by signing in with the Apple ID Offered in the prompt when no key is found
--provider <setup> appwrite, expo or fastlane $APPWRITE_APNS_SETUP, then appwrite
--environment <env> production, sandbox or all all: both providers, and a key for both environments
--force (global) Replace the key on providers that are already set up A rerun says "already set up" and changes nothing

--create-key can't be combined with --key-path or --key-id.

appwrite init push

Flag Meaning
--skip-apns Leave out the APNs setup
--skip-fcm Leave out the FCM setup

Environment variables

Variable Meaning
APPWRITE_APNS_SETUP Default setup for --create-key (appwrite, expo, fastlane)
APPWRITE_APPLE_ID Answers the Apple ID question. The CLI prints the variable's name, never its value.
APPWRITE_APPLE_PASSWORD Answers the password question. The CLI prints the variable's name, never its value.

Usage

Set up push for the whole project

appwrite init push
Detected
  iOS      io.appwrite.stickersmash
  Android  io.appwrite.stickersmash (Firebase project sticker-smash-fb)

ℹ Info: Setting up APNs ...
ℹ Info: Using AuthKey_ABC123DEFG.p8 from the project folder.
✓ Success: Created provider APNs (io.appwrite.stickersmash) ( ... )
✓ Success: Created provider APNs sandbox (io.appwrite.stickersmash) ( ... )
✓ Success: Added the remote-notification background mode to ios/StickerSmash/Info.plist
✓ Success: APNs is set up for io.appwrite.stickersmash.

ℹ Info: Setting up FCM ...

iOS only, with a key already in the project folder

appwrite init apns

The bundle ID and team come from the Xcode project or app.json. The key ID comes from the AuthKey_<KEYID>.p8 file name, so nothing is asked.

Create the key with the Apple ID (one click)

appwrite init apns --create-key
ℹ Info: Setting up APNs ...
ℹ Info: Signing in to the Apple Developer portal (appwrite).
? Apple ID (email) › dev@example.com
? Password for dev@example.com › ********
? Enter the 6-digit code shown on your Apple device (or type sms to get a text message) › 123456
ℹ Info: Using Apple team Example Inc (ABCDE12345).
✓ Success: Created APNs key Q4GAU66T2N on team ABCDE12345 and saved it to AuthKey_Q4GAU66T2N.p8.
♥ Hint: Apple lets you download this key only once. Keep the file somewhere safe and out of version control.
✓ Success: Created provider APNs (io.appwrite.apnstest.native) ( ... )
✓ Success: Created provider APNs sandbox (io.appwrite.apnstest.native) ( ... )
✓ Success: APNs is set up for io.appwrite.apnstest.native.
  • The sign-in is saved in ~/.appwrite/apns/<setup>/ (0600), so later runs skip the password and two-factor until Apple expires it, about 30 days.
  • Without --create-key, the same option is offered in the "How would you like to provide the APNs auth key?" prompt when no key is found.

Skip the Apple ID and password prompts

export APPWRITE_APPLE_ID=dev@example.com
export APPWRITE_APPLE_PASSWORD=...
appwrite init apns --create-key

Only the two-factor code is asked; a saved, trusted session skips that too.

Pick a setup

appwrite init apns --create-key --provider expo       # @expo/apple-utils
appwrite init apns --create-key --provider fastlane   # spaceship via fastlane
export APPWRITE_APNS_SETUP=fastlane                    # default for every run
Setup Needs Installs for you
appwrite (default) nothing nothing
expo nothing extra @expo/apple-utils (npm ci --ignore-scripts, lockfile pinned). Node.js LTS v24.21.0 from nodejs.org when node is missing or older than 18 (about 50 MB, checksum verified, in ~/.appwrite/apns/expo/runtime/).
fastlane fastlane 2.225.0 or later Offers brew install fastlane (asks first) when missing and Homebrew exists. Otherwise prints how to install it, or to use --provider appwrite. Uses bundle exec fastlane when the project's Gemfile (or ios/Gemfile) pins fastlane.

One environment only

appwrite init apns --create-key --environment sandbox
appwrite init apns --key-path AuthKey_ABC123DEFG.p8 --environment production

Only that provider is set up, and a created key is limited to that environment. The expo setup always creates keys for both environments and says so.

CI or scripts (no terminal)

appwrite init apns --key-path AuthKey_ABC123DEFG.p8 --team-id ABCDE12345 --bundle-id com.example.app
  • A missing answer fails with the flag that supplies it, for example Pass --team-id instead.
  • --create-key needs a terminal for the two-factor code. Without one, it explains that and points to --key-path.

Rotate the key

appwrite init apns --key-path AuthKey_NEWKEY1234.p8   # both providers update in place
appwrite init apns --create-key --force                 # create a new key and replace it

When the team has no free key slot

Apple allows two team-scoped APNs keys per team.

⚠ Apple allows two APNs keys per team, and this team has no free slot. Keys on the team:
    Appwrite Push 20261009... (Q4GAU66T2N)
    Firebase (BK454CTD76)
♥ One key works for every app on the team. Reuse a .p8 you saved earlier, or revoke a key you no longer use at https://developer.apple.com/account/resources/authkeys/list.
✗ Error: no APNs key can be created on this team. Pass --key-path with an existing .p8 instead

Rerun

appwrite init apns            # "APNs is already set up for <bundle>", nothing changes
appwrite init apns --force    # replaces the key on the existing providers

How it works

  • Detection:

    • PBXNativeTargets of product type application in .xcodeproj (tests, widgets and extensions are skipped), and Expo app.json.
    • For each target: the bundle ID (resolving $(PRODUCT_NAME:rfc1034identifier)), the team, the entitlements file and Info.plist.
  • Key sources, in order:

    1. --key-path
    2. a valid P-256 .p8 in the project folder
    3. the prompt: Apple ID sign-in, browser (waits for the download in the project folder or ~/Downloads), or a file

    A key that isn't git-ignored gets a warning.

  • Created keys:

    • The .p8 is saved as AuthKey_<ID>.p8 (0600, never overwriting) before anything is sent to Appwrite, since Apple allows one download.
    • The key ID always comes from Apple.
    • A failed --create-key stops instead of falling back to a key file.
  • Setups (internal/apns):

    • Each setup implements apns.Adapter. apns.CreateKey checks the request, then checks what the setup returns: a valid key ID, the requested team, and .p8 contents.
    • The package imports nothing from the CLI, so it can move to its own module.
    • appwrite:
      • Widget key from the App Store Connect sign-out redirect, then SRP-6a against idmsa.apple.com (checked byte for byte against fastlane-sirp), hashcash, two-factor (device code or SMS), and 2sv/trust.
      • Developer portal calls under services-account/QH65B2/: team list, key list (which also hands out the CSRF tokens), v2/create (team scope, APNs for the chosen environment), download.
    • expo: embedded helper.js, run with the terminal attached. EXPO_NO_KEYCHAIN=1 keeps the password out of the Keychain.
    • fastlane: an embedded Fastfile lane, run in a temporary folder. The credentials go in FASTLANE_USER / FASTLANE_PASSWORD, so fastlane never offers to store them.
  • Providers:

    • Two APNs providers (production and sandbox), matched by bundle ID and sandbox flag, created or updated in place.
    • authKey, authKeyId, teamId and bundleId are written through the Appwrite API, like serviceAccountJSON for FCM.
  • Xcode edits: only the chosen bundle's entitlements and Info.plist are changed, keeping their indentation. For Expo, the app.json settings are printed as hints.

Security

The flow was reviewed for credential exposure, file permissions, path traversal, command injection, the download and supply chain, cryptographic randomness, and cookie scope. No issues at high confidence.

  • Passwords and keys never appear in process arguments or logs.
  • Secrets on disk are 0600.
  • Archive unpacking rejects paths that would land outside the folder and skips symbolic links.
  • Downloads are checksum verified, and npm runs with --ignore-scripts against a hashed lockfile.
  • Every subprocess runs with an argument list, never through a shell.
  • The SRP secret comes from crypto/rand.
  • Session cookies only go to Apple hosts.
  • TLS uses the Go standard library defaults.

Testing

  • Unit tests:
    • Xcode detection (app, test, extension and second-app targets) and the plist edits.
    • .p8 validation, the key folder lookup, and providers created, updated and "already set up".
    • The adapter contract and registry, and setup selection (flag, then environment variable, then default).
  • appwrite setup:
    • SRP and hashcash checked against values computed by fastlane's Ruby code.
    • The whole flow against a fake Apple that runs the server side of SRP: two-factor (device and SMS, with wrong codes), a saved, reused and expired session, a full team, team choice, and portal encodings matching Apple's 415 behavior.
  • expo and fastlane setups: stand-in node, npm, fastlane, bundle and brew. Node download with checksum and traversal checks, and Bundler detection.
  • Real Apple account:
    • The native setup signed in (SRP and two-factor by SMS), created key Q4GAU66T2N, saved it and set up both providers.
    • The expo helper created a key earlier.
    • A real Node.js download matched its pinned checksum.
  • Checks:
    • The full CLI suite with -race, gofmt, and generation that is identical across runs.
    • The WebAssembly browser build, and GOOS=windows go vet.
    • The apns tests under Go 1.26.5 (the CLI's minimum).
    • Rector and the Generation suite.

Server dependency

Updating an APNs provider saved the bundle ID under credentials.bundle instead of bundleId, so a changed bundle ID was ignored, and a provider created without one could never be enabled. Rotating keys or rerunning init apns needs that server fix; creating providers works without it.

Not in this PR

  • Topic-specific APNs keys, which are tied to one bundle ID and have more slots per team.
  • Revoking a key from the CLI when the team is full.
  • Listing the Apple account's bundle IDs when the project has no detectable iOS app.

@hansi-codes

hansi-codes Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

🟢 Tier S · Ready to merge

I found no actionable issues in the newly added reset behavior.

This PR adds appwrite init apns and appwrite init push to discover project apps, configure APNs and FCM providers, and apply iOS push entitlements and plist settings. It includes APNs key creation through the built-in Appwrite, Expo, and fastlane setups, with tests for their shared contracts and CLI workflows.

Latest changes: The latest commits add --reset to force a fresh Apple sign-in before creating an APNs key, propagate it through all three setup adapters, and verify the Appwrite session-reset flow and adapter/CLI wiring.

Verdict New comments Fixed Still open
✅ Approved 0 0 0
📂 Walkthrough · 22
File Change
src/SDK/Language/CLI.php Registers the generated CLI's APNs and combined push initialization commands.
templates/cli/internal/cmd/init.go Adds the APNs and push command registrations.
templates/cli/internal/cmd/initpush.go Implements APNs/FCM provider setup, app orchestration, reset handling, and iOS configuration.
templates/cli/internal/cmd/initpushdetect.go Detects Xcode application targets and Expo iOS configuration.
templates/cli/internal/cmd/initpush_test.go Tests app detection, plist edits, provider setup, and reset behavior.
templates/cli/internal/apns/apns.go Defines the APNs key creation contract, shared request, and reset option.
templates/cli/internal/apns/apns_test.go Tests the shared APNs adapter contract.
templates/cli/internal/apns/apnstest/apnstest.go Provides shared adapter test utilities.
templates/cli/internal/apns/appwrite/adapter.go Implements Apple sign-in and key creation, including ignoring the saved session on reset.
templates/cli/internal/apns/appwrite/adapter_test.go Tests built-in sign-in, portal interactions, and fresh-session behavior.
templates/cli/internal/apns/appwrite/auth.go, templates/cli/internal/apns/appwrite/jar.go, templates/cli/internal/apns/appwrite/portal.go, templates/cli/internal/apns/appwrite/srp.go Implements Apple authentication, saved sessions, portal calls, and SRP.
templates/cli/internal/apns/appwrite/srp_test.go Tests the SRP authentication implementation.
templates/cli/internal/apns/expo/adapter.go Runs the embedded Expo key-creation helper and passes reset through to it.
templates/cli/internal/apns/expo/adapter_test.go Tests Expo adapter runtime, helper invocation, and reset wiring.
templates/cli/internal/apns/expo/helper/helper.js Creates APNs keys with apple-utils and clears its saved sign-in for reset.
templates/cli/internal/apns/expo/helper/package.json, templates/cli/internal/apns/expo/helper/package-lock.json Pins the embedded Expo helper dependency.
templates/cli/internal/apns/expo/runtime.go Acquires and verifies a compatible Node.js runtime when needed.
templates/cli/internal/apns/expo/runtime_test.go Tests Node.js runtime acquisition and validation.
templates/cli/internal/apns/fastlane/adapter.go Runs the embedded fastlane lane and passes reset through to it.
templates/cli/internal/apns/fastlane/adapter_test.go Tests fastlane version handling, invocation, and reset wiring.
templates/cli/internal/apns/fastlane/helper/Fastfile Creates APNs keys with spaceship and clears its saved session for reset.
templates/cli/internal/apns/appwrite/portal.go Sends empty Apple Developer portal calls without a JSON body.

Reviewed the commits since bb3e678 · Details · Comment @hansi-codes review to re-run, or mention @hansi-codes with a question.

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Tier A · Looks good to merge. Summary

Comment thread templates/cli/internal/cmd/initpush_test.go Outdated

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Tier S · Looks good to merge. Summary

Apple has no public API for creating APNs keys, and the tools that do it
(spaceship, @expo/apple-utils, Appwrite's own) all drive the Developer
portal's private one, which Apple can change at any time. Each becomes an
Adapter behind one contract: CreateKey checks the request, runs the
adapter and checks the key it returns (key ID, team, .p8 contents), so
every setup behaves the same to the caller. A Registry finds them by name.

The package imports nothing from the CLI: prompts, logging, credentials
and the session folder come in through the Request, so it can move to
its own module unchanged. apnstest has a scripted adapter for tests.
--create-key creates the key with the setup picked by --provider, then
APPWRITE_APNS_SETUP, then appwrite, so a setup Apple breaks can be
swapped without a release. APPWRITE_APPLE_ID and APPWRITE_APPLE_PASSWORD
answer the sign-in questions, and the CLI names the variable it used.
--environment (production, sandbox or all) picks the providers to set up
and the environment of a created key. Without a known team, the sign-in
chooses it. The new .p8 is saved before the providers are written, since
Apple allows one download, and a failed --create-key stops rather than
falling back to a key already in the folder.

No setup is registered yet: each comes in its own change, and until then
the sign-in option is not offered.
A key created by the Apple sign-in has an ID Apple chose. --key-id could
override it, and the providers then got an ID that does not match the
.p8, so every push would fail to authenticate. --create-key now rejects
--key-id, and a key created from the prompt always uses Apple's ID,
with a warning when --key-id said otherwise.

Sign-in questions asked without a terminal also no longer suggest
--key-path, which --create-key rejects. The error names the question,
says APPWRITE_APPLE_ID and APPWRITE_APPLE_PASSWORD answer the Apple ID
and password, that the two-factor code always needs a terminal, and to
use --key-path instead of --create-key otherwise.
The appwrite adapter signs in to Apple and calls the Developer portal in
Go, with nothing to install. The sign-in follows spaceship: the widget
key from the App Store Connect sign out redirect, SRP-6a against
idmsa.apple.com (PBKDF2 over the password, s2k and s2k_fo), hashcash,
two-factor by device code or text message, and a trusted session kept
in the request's session folder so later runs skip two-factor. The key
calls follow @expo/apple-utils: list the team's keys, which hands out
the CSRF tokens, create a team-scoped key with APNs for the requested
environment, and download the .p8.

The SRP and hashcash code is checked against values computed by
fastlane-sirp and spaceship, and the whole flow against a fake Apple
that runs the server side of SRP. The Go bodies of portal.go and
adapter_test.go are Twig verbatim blocks, as their nested composite
literals read as Twig tags.
Registers the appwrite adapter, the default for --create-key, so the
Apple sign-in is offered and --provider appwrite (or
APPWRITE_APNS_SETUP=appwrite) selects it.
The expo adapter creates APNs keys with @expo/apple-utils 2.2.1 (MIT),
the library eas-cli uses. A small Node script, its package.json and a
lockfile pinning the package hash are embedded with go:embed and
installed into the session folder with npm ci --ignore-scripts when they
change. It needs Node.js 18 or later and reports apns.ErrUnavailable
otherwise, before contacting Apple.

apple-utils prompts on the terminal for the two-factor code and for
anything the request leaves out; the request's Apple ID and password
are passed as EXPO_APPLE_ID and EXPO_APPLE_PASSWORD, and
EXPO_NO_KEYCHAIN=1 keeps the password out of the macOS Keychain. Its
InvalidUserCredentialsError maps to apns.ErrInvalidCredentials and a full
team to apns.MaxKeysError. apple-utils always creates keys for both
environments, which also serve a single-environment request.
Registers the expo adapter, picked with --provider expo or
APPWRITE_APNS_SETUP=expo.
The fastlane adapter creates APNs keys with spaceship, fastlane's
open-source Apple Developer portal client, through the fastlane the
person already has installed: a lane in an embedded Fastfile runs in a
private temp folder, so a gem, Homebrew or bundler installation all work
and no Ruby ships with the CLI. It needs fastlane 2.226.0 or later, the
first to sign in with SRP, and reports apns.ErrUnavailable otherwise.

The adapter asks for the Apple ID and password when the request has
none and passes them as FASTLANE_USER and FASTLANE_PASSWORD, so fastlane
never offers to store them in the Keychain; spaceship asks for the
two-factor code on the terminal. The lane calls create_key! directly so
the requested environment is honoured (Key.create always asks for all),
maps InvalidUserCredentialsError to apns.ErrInvalidCredentials and a
full team to apns.MaxKeysError with the team's keys.
Registers the fastlane adapter, picked with --provider fastlane or
APPWRITE_APNS_SETUP=fastlane.
2.225.0 (October 2024) is the release that added SRP sign-in, per its
notes: earlier versions get 503 from Apple. The minimum was set one
release too high.
Typing sms to get the code by text message used one of the three
attempts, leaving two for actual codes. Only codes Apple rejects count
now.
The hashcash date was the machine's local time. @expo/apple-utils sends
UTC (spaceship's local time only matches on a UTC machine), so a
non-UTC machine produced a date hours away from the one Apple expects.
The fastlane setup ran fastlane from PATH in a temporary folder, so a
fastlane installed only through a project's Gemfile was reported as
unavailable. When the Gemfile in the folder the CLI runs in, or in its
ios folder as Flutter and React Native projects keep it, lists fastlane
and bundle is installed, the lane now runs with bundle exec fastlane and
BUNDLE_GEMFILE pointing at that Gemfile. Otherwise fastlane from PATH is
used as before. A Bundler fastlane that does not start asks for bundle
install.
The CLI leaves the team out when none is known and lets the sign-in pick
it. The test now checks that the helper then gets no --team-id.
When the project's Gemfile lists fastlane but Bundler is missing, the
fastlane setup fell back to fastlane from PATH, which can be a different
version from the pinned one. It now reports apns.ErrUnavailable naming
the Gemfile and how to install Bundler.
…g-provider-cli-apns-adapters

# Conflicts:
#	src/SDK/Language/CLI.php
#	templates/cli/internal/cmd/initpush.go
…aging-provider-cli-apns-adapters

# Conflicts:
#	src/SDK/Language/CLI.php
#	templates/cli/internal/cmd/initpush.go

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Tier A · See the inline comments. Summary

Comment thread templates/cli/internal/apns/appwrite/auth.go Outdated
Any difficulty up to 64 bits was answered, and the search ignored
cancellation, so an abnormal challenge could keep init apns spinning
indefinitely. Challenges above 24 bits (Apple asks for about 11; 24 is
some 16 million attempts) are now left unanswered, as one sent without a
challenge is, and the search checks the context every 65536 attempts so
Ctrl-C stops it.

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Tier S · Looks good to merge. Summary

The expo setup stopped when node or npm was missing, or node was older
than 18. It now downloads the Node.js LTS (v24.21.0) for this OS and CPU
from nodejs.org into the setup's session folder, checks the archive
against the SHA-256 pinned for each official build, unpacks it beside
its final folder and moves it into place, and runs npm through node.
Nothing is installed system-wide, and later runs reuse it. Entries that
would unpack outside the folder are refused and symbolic links (npm's
shell wrappers) are skipped. A node set on the adapter is checked but
never replaced.
The fastlane APNs key setup stopped with a bare 'needs fastlane' when
none was installed. When Homebrew is available it now asks to run brew
install fastlane on the terminal and carries on. Without Homebrew, a
terminal or a yes, it says how to install fastlane (Homebrew, gem or the
project's Gemfile) or to use --provider appwrite. An installed fastlane
older than 2.225.0 now gets the matching upgrade command: bundle update
fastlane for a project's Bundler fastlane, otherwise brew upgrade or gem
update. A project's pinned fastlane and an explicit command are never
replaced by an install.
Two expo setups starting together in the same session folder could both
download Node.js, and the later one removed the runtime the earlier one
had just published, maybe while it was running npm or the helper.
Publishing is now only a rename: an existing runtime is never removed,
and when another run published first, its complete runtime is used.
…er hint

A fastlane command set on the adapter was offered a Homebrew install
when it was missing, which could replace a deliberate choice; the offer
is now only for fastlane from PATH. The too-old message for a project's
Bundler fastlane said 'bundle update fastlane in <folder>', which Bundler
reads as gem names; it now says 'cd <folder> && bundle update fastlane'.
cd <folder> && bundle update fastlane broke for folders with spaces: the
shell split the path. The folder is now quoted for the shell people
paste into, single quotes on Unix (with a single quote in the name
escaped) and double quotes on Windows, and left as is when it has only
safe characters.
Double quotes are not safe on Windows: PowerShell expands $name and $()
inside them, so a folder name could change the pasted command or run an
expression, cmd expands %, and Windows PowerShell has no &&. No single
quoting works in every Windows shell, so the Bundler upgrade hint there
names the folder to run bundle update fastlane from. Unix keeps the
single-quoted cd command.
The appwrite APNs setup signed in, then failed with HTTP 415 from
account/listTeams.action: every portal POST carried a JSON body, and
that endpoint, like the portal's other .action endpoints, refuses one.
apple-utils and spaceship send it with no body. A portal call with
nothing to send now has no body and no Content-Type. The fake portal in
the tests answers a body there with 415, as Apple does.
After the team list, the appwrite APNs setup failed with HTTP 415 from
account/auth/key/list: portal POSTs carried JSON. apple-utils and
spaceship send them form encoded (application/x-www-form-urlencoded) and
use JSON only to create a key. Portal POSTs are now forms, key creation
stays JSON, and a call with nothing to send has no body. The fake portal
in the tests accepts only those encodings, answering others with 415 as
Apple does.
Apple now answers a team with no free APNs key slot with 'You have
already reached the maximum allowed number of team scoped Keys for this
service in production and sandbox environment.' All three setups looked
for 'maximum allowed number of Keys', so the error came through raw
instead of listing the team's keys with the --key-path advice. They now
match the message with or without the words before 'Keys', and the expo
helper also checks the error's own message, since apple-utils does not
turn the new wording into MaxKeysCreatedError either.

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛔ Tier F · 1 blocking finding to address. Summary

Comment thread templates/cli/internal/apns/appwrite/portal.go
A saved Apple sign-in was reused until Apple expired it, and the only
way to sign in again, or as another Apple ID, was to delete a file by
hand. --reset signs in from scratch, two-factor included, and the new
session replaces the saved one. It implies --create-key, as signing in
is only for creating a key, so it is not stopped by APNs being set up
already, and it cannot be combined with --key-path or --key-id.

apns.Request gains Reset. The appwrite setup loads none of the saved
cookies, not even the one that marks the machine as trusted. The expo
helper runs apple-utils' Auth.logoutAsync, which deletes its saved
session for the Apple ID, and the fastlane lane deletes spaceship's
saved session for the Apple ID before signing in.
ArnabChatterjee20k and others added 2 commits October 9, 2026 17:20
With --reset and no APPWRITE_APPLE_ID, the expo setup asked for the
Apple ID twice: apple-utils' Auth.logoutAsync asks for it to find the
session and forgets it, and the sign-in that follows asked again. The
adapter now asks once and passes it as EXPO_APPLE_ID, which both read
(resolveCredentialsAsync takes the given username, then EXPO_APPLE_ID,
and prompts only without either).
feat(cli): init apns --reset signs in to Apple again

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Tier S · Looks good to merge. Summary

@ArnabChatterjee20k ArnabChatterjee20k changed the title feat(cli): init apns and init push for Appwrite Push providers feat(cli): init apns and init push, with one-click APNs key creation Oct 9, 2026
@ArnabChatterjee20k

Copy link
Copy Markdown
Member Author

Closing in favour of #2015, which has appwrite init apns on its own branch from main, with everything from this PR and the fixes since. Splitting it from init fcm (#1956), which is blocked on permissions, lets APNs merge on its own. init push is dropped: init apns and init fcm run separately.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant