payload-plugin-android-upload
v1.0.3
Published
Payload CMS plugin for Android share-target media uploads with a permission-aware API and share UI
Maintainers
Readme
payload-plugin-android-upload
Payload CMS plugin for mobile share-target uploads. Lets Android (and future iOS) clients authenticate to Payload, discover upload collections, and send media directly into upload collections such as media — with a permission-aware API and a lightweight web UI that reuses Payload’s own create form.
Built for Payload 3.x and Next.js.
Features
- Client setup endpoint — one authenticated request returns available collections, the share-target URL, and the upload API template
- Permission-aware collection discovery — respects collection-level
createaccess; only collections the user can create appear - Upload collection focus — by default targets Payload upload collections (
collection.upload, e.g.media), not every collection with an upload field - Multipart upload API — native apps can POST files directly without the admin panel
- Share-target web flow — PWA manifest + standalone routes for Android share sheet / WebView
- Payload create form — upload collections render the same
Form+RenderFieldsfield components as admin create (without admin chrome) - Stateless plugin — no extra collections, schema changes, or seed data added by the plugin itself
Installation
pnpm add payload-plugin-android-uploadPeer dependencies: payload ^3.84.1, @payloadcms/ui ^3.84.1, react, and react-dom
Quick start
Add the plugin to your Payload config:
import { buildConfig } from 'payload'
import { payloadPluginAndroidUpload } from 'payload-plugin-android-upload'
export default buildConfig({
// ...
plugins: [
payloadPluginAndroidUpload({
// optional — see Configuration
}),
],
})Wire up the share UI routes in your Next.js app (see Host app routes).
Configuration
payloadPluginAndroidUpload({
/** Whitelist specific collections. If omitted, auto-detect applies. */
collections?: ['media'],
/**
* When true, also include document collections with upload fields
* (e.g. posts with featuredImage). Default: false.
*/
includeDocumentCollections?: false,
/** Base path for the standalone share UI. Default: '/mobile-upload' */
basePath?: '/mobile-upload',
/** Override share POST handler path. Default: '{basePath}/share' */
shareTargetPath?: '/mobile-upload/share',
/** Disable endpoints while keeping the package installed */
disabled?: false,
})How collections are detected
| Type | Identified by | Example | Included by default? |
|------|---------------|---------|----------------------|
| Upload collection | upload set on collection config | media | Yes |
| Document with upload fields | type: 'upload' fields on a normal collection | posts → featuredImage | No (use includeDocumentCollections: true or whitelist) |
Each collection in API responses includes isUploadCollection: true | false.
API
All endpoints require authentication. Pass the Payload JWT from login:
Authorization: JWT <token>Base path: /api (or your configured routes.api).
GET /api/mobile-upload/config
Primary endpoint for native app setup. Call once after the user logs in to Payload.
Example response:
{
"collections": [
{
"slug": "media",
"labels": { "singular": "Media", "plural": "Media" },
"isUploadCollection": true,
"uploadFields": [
{ "name": "file", "relationTo": "media", "hasMany": false }
]
}
],
"shareTarget": {
"url": "https://your-site.com/mobile-upload/share",
"method": "POST",
"enctype": "multipart/form-data"
},
"upload": {
"urlTemplate": "https://your-site.com/api/mobile-upload/{collectionSlug}",
"method": "POST"
}
}| Field | Use |
|-------|-----|
| collections | Populate the in-app collection picker |
| shareTarget.url | Android share intent / WebView target |
| upload.urlTemplate | Replace {collectionSlug} when uploading from the native app |
GET /api/mobile-upload/collections
Returns { collections: UploadTarget[] } — same collection list as config, without share/upload URLs. Kept for backwards compatibility.
POST /api/mobile-upload/:collectionSlug
Multipart upload endpoint for native clients.
Upload collection (media):
- Body:
multipart/form-datawith one or more files underfile - Optional metadata via
_payloadJSON field (e.g.{ "alt": "Description" }) - Response:
{ docs: [...], errors: [...] }
Document collection (only when enabled via includeDocumentCollections or whitelist):
- Requires
uploadFieldin_payloadwhen the collection has multiple upload fields - Uploads files to the related upload collection, then creates the document
All creates use overrideAccess: false and respect Payload access control.
Native app flow
sequenceDiagram
participant App as Native app
participant Payload as Payload API
App->>Payload: POST /api/users/login
Payload-->>App: JWT
App->>Payload: GET /api/mobile-upload/config
Payload-->>App: collections, shareTarget, upload template
App->>App: User picks collection
App->>Payload: POST /api/mobile-upload/media (multipart + JWT)
Payload-->>App: Created media docsWeb share-target UI
Standalone mobile-friendly pages (no admin sidebar). Requires an authenticated Payload session or JWT.
| URL | Purpose |
|-----|---------|
| /mobile-upload | Collection picker |
| /mobile-upload/:collection | Payload create form for that collection |
| /mobile-upload/share | POST handler for Android share sheet (redirects to picker) |
| /manifest.webmanifest | PWA manifest with share_target (dev harness) |
For upload collections, the create form uses Payload’s buildFormState, Form, and RenderFields — the same field components as admin create, submitted to POST /api/:collectionSlug.
Unauthenticated users are redirected to /admin/login?redirect=....
Host app routes
The plugin registers API endpoints automatically. The share UI is exported for your Next.js app under the Payload layout.
Example pages (see dev/app/(payload)/mobile-upload/):
// app/(payload)/mobile-upload/page.tsx
import config from '@payload-config'
import { ShareUploadPage } from 'payload-plugin-android-upload/rsc'
import { headers as getHeaders } from 'next/headers'
import { redirect } from 'next/navigation'
import { createPayloadRequest } from 'payload'
export default async function MobileUploadPage({ searchParams }) {
const { filesToken } = await searchParams
const headersList = await getHeaders()
const host = headersList.get('host') ?? 'localhost:3000'
const protocol = headersList.get('x-forwarded-proto') ?? 'http'
const req = await createPayloadRequest({
config,
request: new Request(`${protocol}://${host}/mobile-upload`, {
headers: headersList,
}),
})
if (!req.user) redirect('/admin/login?redirect=/mobile-upload')
return <ShareUploadPage filesToken={filesToken} pluginOptions={{}} req={req} />
}PWA manifest (optional, for Android share target):
// app/manifest.ts
export default function manifest() {
return {
name: 'Payload Mobile Upload',
start_url: '/mobile-upload',
share_target: {
action: '/mobile-upload/share',
method: 'POST',
enctype: 'multipart/form-data',
params: {
files: [{ name: 'files', accept: ['image/*', 'video/*', 'application/pdf'] }],
},
},
}
}Package exports
| Import | Description |
|--------|-------------|
| payload-plugin-android-upload | Plugin factory + types |
| payload-plugin-android-upload/client | CollectionPicker, ShareUploadForm |
| payload-plugin-android-upload/rsc | ShareUploadPage server component |
| payload-plugin-android-upload/share | Share file store helpers |
| payload-plugin-android-upload/files | getFilesFromRequest utility |
Local development
This repo includes a dev harness in dev/.
pnpm install
pnpm dev| URL | Description | |-----|-------------| | http://localhost:3000/admin | Payload admin | | http://localhost:3000/mobile-upload | Share upload picker | | http://localhost:3000/mobile-upload/media | Create media form | | http://localhost:3000/api/mobile-upload/config | Client setup API | | http://localhost:3000/manifest.webmanifest | PWA manifest |
Default dev login:
| | |
|---|---|
| Email | [email protected] |
| Password | test |
Seeded automatically on first startup via dev/seed.ts.
Database
Dev uses SQLite by default (dev/payload.db) — no MongoDB required.
To use MongoDB instead, create dev/.env:
DATABASE_URL=mongodb://127.0.0.1/payload-plugin-android-upload
PAYLOAD_SECRET=dev-secretSee dev/.env.example.
Scripts
| Command | Description |
|---------|-------------|
| pnpm dev | Start Next.js dev server |
| pnpm build | Build plugin to dist/ |
| pnpm test:int | Integration tests (Vitest) |
| pnpm test:e2e | Playwright admin smoke test |
| pnpm generate:types | Regenerate Payload types for dev harness |
Dev collections
The dev harness includes:
media— upload collection (altfield, local files indev/media/)posts— document collection withfeaturedImageandgalleryupload fields (only whenincludeDocumentCollections: true)users— auth collection
Security notes
- All plugin endpoints require
req.user; unauthenticated requests return401 - Local API calls use
overrideAccess: falseso collection access rules apply - Set
serverURLin Payload config in production so client setup URLs are absolute and correct
Releasing
Versioning, git tags, GitHub releases, and npm publishes are handled automatically when work merges to main.
Day-to-day workflow
- Commit on
devusing Conventional Commits - Open a PR from
dev→main - Merge the PR
- GitHub Actions runs semantic-release, which:
- reads commits since the last tag
- bumps
package.jsonand commits it tomain(patch / minor / major) - updates
CHANGELOG.md - creates a GitHub Release
- publishes to npm
- creates a git tag (e.g.
v1.1.0)
After a release, merge main back into dev so both branches stay in sync on version and changelog.
Commit message → version bump
| Commit prefix | Example | Version bump |
|---------------|---------|--------------|
| fix: | fix: dedupe files from multipart request | patch (1.0.0 → 1.0.1) |
| feat: | feat: add iOS share target support | minor (1.0.0 → 1.1.0) |
| feat!: or BREAKING CHANGE: in body | feat!: rename config option | major (1.0.0 → 2.0.0) |
| docs:, chore:, test:, etc. | docs: update README | no release |
PR titles are checked in CI; squash-merge PRs using a conventional title (e.g. feat: add collection picker) so the commit on main is parseable.
One-time setup
GitHub secret
NPM_TOKEN— create a Granular Access Token on npmjs.com → Access Tokens:- Permissions: Read and write for your package (or all packages)
- Enable Bypass two-factor authentication for automation (required for CI; otherwise publish fails with
EOTP) - Add the token as
NPM_TOKENin GitHub → Settings → Secrets and variables → Actions
GITHUB_TOKENis provided automatically for GitHub Releases.First release tag — before relying on automation, tag the current
maincommit if1.0.0is already released (or about to be released manually):git checkout main git pull git tag v1.0.0 git push origin v1.0.0semantic-release uses git tags (not
package.json) as the source of truth for “what’s already released”. Without a tag, the first automated release may calculate the wrong baseline.Default branch — set GitHub’s default branch to
mainso release PRs and tags land on the release branch.
If a release workflow fails mid-way
semantic-release runs GitHub Release before npm publish. If npm fails (e.g. invalid token), you may see a git tag and an updated package.json on main without a GitHub Release or npm package. Fix NPM_TOKEN, then either:
- Re-run the Release workflow (Actions → Release → Run workflow), or
- Manually publish from
main:pnpm build && npm publish --access public
License
MIT
