The APK
ReadyInstall it or share it
rekha-pottery-1.0.0-1.apk
- Package id
- ai.openally.app.a58yjxke7pbbs5py4sw8enj704g
- Version
- 1.0.0 (1)
- Signed
- With this app's own key
On the build card
- Save
- Share
- Open
Web to App is an OpenAlly mini-app that turns a website you run into an Android app, free on every plan. Paste the site's address: OpenAlly reads its name, colours and icon, then builds a signed APK to install or share — and an AAB for your own Google Play upload — on your Android phone or Mac. A build takes as little as 10 seconds, usually under a minute; the first build also creates your signing key.
Builds on
Not on iPhone.

Type an address and follow the steps a real build takes: OpenAlly reads the site, prepares the signing key and the icons, then builds, signs and checks the APK and, once your key is backed up, the AAB. This demo runs in the page; nothing is sent anywhere.
Finished
Rekha Pottery
Made with OpenAlly
On the home screen
In OpenAlly these come from your site itself — its name, colours and icon — and you can change any of them before you build.
Install it or share it
rekha-pottery-1.0.0-1.apk
On the build card
For your own Google Play upload
rekha-pottery-1.0.0-1.aab
On the build card
The same four steps in the Web to App mini-app on your Android phone or Mac, or in a chat with your AI.
Paste the fixed https address of a site you run. A path is fine; a temporary tunnel or a link that carries a token is not.
OpenAlly brings your site's name, colours and icon with it. Change any of them, write a welcome line, or create an icon with AI.
Build a signed APK, plus an AAB once your signing key is backed up. Building and signing stay on your Android phone or Mac.
Save the APK and tap it to install, share it with anyone, or upload the AAB to your own Google Play listing.
OpenAlly reads the details your site publishes for browsers and link previews, never the content of its pages, and treats each one as a suggestion you can change.
Every switch lives in the app's Android setup. Try them here: the two lists show what the built app will ask for and what your page will be able to reach.
Welcome text
A headline and a supporting line on the welcome screen while your site opens.
Home-screen icon
Keep your site's icon, upload an image, or choose Create my icon with AI, using your connected image provider or Premium image credits.
Status-bar icons
Light or dark, so they read on your colours.
Orientation
Follow the phone, Portrait or Landscape.
Only you can switch on camera, microphone, location, notifications and clipboard reading, or choose Classic and new for the newer page features. Your AI can switch any of them off, never on.
Colour key
Screen and navigation
Pull to refresh
Let people pull down at the top of a page to reload it.
Files, photos and sharing
Downloads
Let the app save files the page offers, such as an invoice or a receipt.
Screen and navigation
Play media without a tap
Let video and audio start on their own. Off means the person taps play first.
Notifications and haptics
Vibration and haptics
Let the page make the phone buzz on a tap. Off removes the vibration permission, and taps stay silent.
Files, photos and sharing
Text selection and copying
Let people select text on the page and copy it. Off also stops the page writing to the clipboard.
Links to other websites
Choose where links outside your website open.
Files, photos and sharing
Camera
For taking a photo or scanning a code from inside the app.
Only you can switch this on
Location and permissions
Microphone
For recording audio or taking voice input from inside the app.
Only you can switch this on
Location and permissions
Location
For finding where the person is, such as filling in a delivery address.
Only you can switch this on
Notifications and haptics
Notifications
For the app to post a notification on the phone it is installed on.
Only you can switch this on
Each switch does two things: it lets the page use that part of the phone, and it decides whether the built app asks for the matching permission at all. Anything left off is stripped out, so the install screen and the Play listing show only what you turned on. Notifications are ones your page posts on the phone.
Available to the page
What the page inside your app is allowed to ask the phone to do. Classic is the small set every app has had; Classic and new adds file pickers, location, the clipboard and screen control.
Only you can switch this on: Classic and new
These three are part of the newer surface, the App2Web SDK. Choose Classic and new above to use them.
Files, photos and sharing
File and photo picking
Let the page open the photo and file pickers and save a file to Downloads. The person always chooses the file, so this asks for no permission.
Files, photos and sharing
Clipboard reading
Let the page read what is on the clipboard, not just write to it. Android shows people a notice every time the page reads it.
Only you can switch this on
Screen and navigation
Screen control
Let the page lock the orientation, keep the screen awake, restyle the status bar and handle the back button itself.
Every build is signed with your app's own key, on your device. The APK installs on Android 7.0 and newer; the AAB is the file Google Play takes.
An installable Android app to save, share and try on a device. It installs on Android 7.0 and newer when you tap it.
A Play Console upload bundle, built with an APK for testing the same release. It unlocks once your signing key is backed up.


Every Android app is signed, and the signature is what lets a phone accept a new version as an update. Your app's key is created on your device with its first build and lives there. Back it up once, to a file protected by a passphrase you choose, and the AAB unlocks — uploading one commits your Play listing to that key for good.
Lose a key with no backup and no later version of the app can be accepted as an update.
Each app gets an id that starts with ai.openally.app when it is created. It never changes, which is what lets every later version install as an update.
Each upload needs a higher version code than the last. Reusing the last one replaces a build you have not uploaded.
Each APK and AAB stays under Generated files, ready to save or share again, until you delete it.
The next build opens the new address. Copies already installed keep the old one.
Removes its setup, builds and signing key from OpenAlly. Your website, installed copies and files you already saved or shared stay.
Build your AAB here, then follow four steps in Play Console. Your publishing status lives there, and the upload is yours to make.
Start with your developer account
Open a Google Play developer account, then create your app in Play Console. Choose its name and language, and add a contact email.
Give your app a storefront
Give people a reason to install: a clear description, app icon, screenshots and support details that show what your app can do.
Test the version you will ship
Use a higher version code for each upload, meet Google's current target API level requirement, and complete the testing your account needs before production.
Bring your app to people
Choose your countries, pricing and release notes. Resolve the issues shown in Play Console, then send your changes for review.
Google Play's policy on apps with limited functionality does not allow "apps that only have limited functionality and content", so an app that only opens a website is judged on what that website does for the person using it.

In a chat, your AI can turn your website into an Android app with the Web to App skill: it checks the site, creates the app, builds it and hands you the files.
Checked the site
It reads the name, colours and icon, and tells you in plain words if the address will not work.
Created an app from your link
With the name you gave, or the one your site suggests.
Started an Android build
An APK by default, and the AAB as well once your key is backed up.
The build card
Save, Share and Open, right in the chat.
The first build on an Android phone needs a small app component. In the app a download card appears in the chat; on a messaging channel your AI sends these steps instead:
On a Mac there is nothing to download.
Turn my website into an Android app: https://rekha-pottery.example.com

An App Builder app becomes an Android app the same way, once it has a fixed https address for the installed app to open.
window.OpenAllyApp2Web — SDK 1.0.0, 51 methods across 16 namespaces, 9 event streams. Once the app's owner sets its Web-to-app bridge to Classic and new, the app injects it at document start, top frame only; nothing to install or import.
In the app's Android setup, set Web-to-app bridge to Classic and new — the default is Classic only, the seven window.OpenAllyShell calls every app has had — then switch on what the page may use.
Built your app in the App Builder? It already writes these checks, and every app it scaffolds carries the types.
On your own website, check window.OpenAllyApp2Web after DOMContentLoaded: the app injects it before your scripts run, and an ordinary browser simply has none, so the same page keeps working there.

App and device
Screen and navigation
Location and permissions
Files, photos and sharing
Notifications and haptics
Location and permissions
Where the person is, with their permission.
| Method | What it does | Switched on by | Android permission |
|---|---|---|---|
getLocation(options?: GetLocationOptions): Promise<LocationFix> | A fresh position, or a recent one within maxAgeMs. Pass precise: true for fine location. | Location | Location android. |
getLastKnown(options?: LastKnownOptions): Promise<LocationFix | null> | The last position the phone knows, or null. Read timestampMs for its age. | Location | Location android. |
const sdk = window.OpenAllyApp2Web; // absent in an ordinary browser
if (sdk) await sdk.whenReady();
if (sdk && sdk.available && sdk.supports("location")) {
let status = await sdk.permissions.check("location");
if (status.state !== "granted" && status.canAsk) {
status = await sdk.permissions.request("location"); // getLocation never asks by itself
}
if (status.state === "granted") {
try {
const fix = await sdk.location.getLocation({ timeoutMs: 8000 });
showNearestStore(fix.latitude, fix.longitude);
} catch (e) {
askForPostcode(); // e.code says why, e.g. "E_TIMEOUT"
}
} else {
askForPostcode(); // status.remedy is written for the app's owner
}
} else {
askForPostcode();
}
OpenAllyApp2Web.on(type, handler) returns a function that stops it. A page can listen to 8 event types at once.
| Event | Switched on by | Delivery | At most every | Current value on subscribe |
|---|---|---|---|---|
lifecycle | Always on | Every event | None | Yes |
network | Always on | Latest value wins | 250 ms | Yes |
battery | Always on | Latest value wins | 30 s | Yes |
insets | Always on | Once it settles | 50 ms | Yes |
keyboard | Always on | Once it settles | 50 ms | Yes |
orientation | Always on | Only when it changes | 100 ms | Yes |
colorScheme | Always on | Only when it changes | 100 ms | Yes |
location | Location | At the interval you pass to location.watch({ minIntervalMs, minDistanceM }, onFix) | None | No |
notification | Notifications | Every event | None | No |
Every failure is a thrown error with a code. Narrow it with OpenAllyApp2Web.isError(e), then branch on e.code. Two things are values, never errors: a permission check that says no, and a picker the person dismissed.
| Code | What it means | Trying again can help |
|---|---|---|
E_UNAVAILABLE | No host at all: an ordinary browser, or a shell that injected nothing. | No |
E_DETACHED | The app's page was closed or replaced while the call was running. | Yes |
E_FRAME_UNTRUSTED | A frame that is not the trusted top frame tried to call. | No |
E_SUBFRAME_UNSUPPORTED | A same-origin sub-frame's own port. Refused in v1. | No |
E_UNSUPPORTED_METHOD | This host does not implement the method — usually an older app shell. | No |
E_FEATURE_DISABLED | The capability was not switched on when the app was built. | No |
E_PERMISSION_NOT_DECLARED | The app was built without that permission, so it can never be granted. | No |
E_PERMISSION_DENIED | The permission has not been given: not asked yet, or the person said no this time. | Yes |
E_PERMISSION_BLOCKED | The person said no permanently, or policy forbids asking. | No |
E_CANCELLED | The person dismissed a system UI. Only where cancellation is not a value. | No |
E_INVALID_ARG | An argument was missing, the wrong type, or out of range. | No |
E_TOO_LARGE | A payload or read exceeded its documented ceiling. | No |
E_TIMEOUT | The call did not settle in time. | Yes |
E_POLICY_REFUSED | The SDK or the host refused on policy grounds (e.g. too many subscriptions). | No |
E_BUSY | A single-flight slot is occupied — a picker is already open. | Yes |
E_ABANDONED | The document was replaced, or the process died, before the call settled. | Yes |
E_IO | A read or write failed. | Yes |
E_INTERNAL | An unexpected host-side fault. Carries no detail in a release build. | No |
| Method | What it does | Switched on by | Android permission |
|---|---|---|---|
app.getInfo | getInfo(): Promise<AppInfo> The app's own name, version, version code, package id and build. | Always on | None |
app.getCapabilities | getCapabilities(): Promise<CapabilitiesInfo> Which capabilities this build answers, plus a cached permission snapshot for greying out a button. | Always on | None |
device.getInfo | getInfo(): Promise<DeviceInfo> Model, maker, brand and Android version. No identifier of any kind. | Always on | None |
device.getLocale | getLocale(): Promise<LocaleInfo> Preferred languages, time zone, UTC offset and the 12- or 24-hour preference. | Always on | None |
ui.getInsets | getInsets(): Promise<Insets> Safe-area insets in dp, for padding around the camera cut-out and the gesture bar. | Always on | None |
ui.getOrientation | getOrientation(): Promise<OrientationInfo> Portrait or landscape, right now. | Always on | None |
ui.getMetrics | getMetrics(): Promise<ScreenMetrics> Viewport size in dp, pixel density, orientation and colour scheme in one read. | Always on | None |
ui.getKeyboard | getKeyboard(): Promise<KeyboardState> Whether the on-screen keyboard is up, and how much of the viewport it covers. | Always on | None |
ui.getColorScheme | getColorScheme(): Promise<ColorSchemeInfo> Light or dark, as the person has set the phone. | Always on | None |
ui.setStatusBar | setStatusBar(options: StatusBarOptions): Promise<UiWriteResult> Restyle the status bar. It resets when the page is replaced. | Screen control | None |
ui.lockOrientation | lockOrientation(options: LockOrientationOptions): Promise<UiWriteResult> Pin the rotation, or hand it back to the app's setting. Released when the page is replaced. | Screen control | None |
ui.setKeepAwake | setKeepAwake(options: KeepAwakeOptions): Promise<UiWriteResult> Keep the screen on. Dropped when the page is replaced. | Screen control | None |
haptics.vibrate | vibrate(options?: VibrateOptions): Promise<VibrateResult> One of five feedback kinds: light, medium, heavy, success or error. | Vibration and haptics | Vibration android. |
navigation.openExternal | openExternal(options: OpenExternalOptions): Promise<OpenExternalResult> Hand a link to the phone, and learn whether anything opened it. | Always on | None |
navigation.openInApp | openInApp(options: OpenInAppOptions): Promise<OpenExternalResult> Open a web page in a browser sheet over the app, where the phone has one, whatever the Links to other websites setting says. Pass toolbarColor to tint it. | Always on | None |
navigation.setBackHandler | setBackHandler(handler: BackHandler | null): void Take over the back button: return true when your page handled the press, or pass null to give it back. Answer within 300 ms. | Screen control | None |
location.getLocation | getLocation(options?: GetLocationOptions): Promise<LocationFix> A fresh position, or a recent one within maxAgeMs. Pass precise: true for fine location. | Location | Location android. |
location.getLastKnown | getLastKnown(options?: LastKnownOptions): Promise<LocationFix | null> The last position the phone knows, or null. Read timestampMs for its age. | Location | Location android. |
permissions.check | check(name: PermissionName): Promise<PermissionStatus> Where a permission stands: granted, denied, blocked or unavailable. It never throws for a refusal. | Always on | None |
permissions.request | request(name: PermissionName, options?: PermissionRequestOptions): Promise<PermissionStatus> Ask with the system dialog, at most once per capability per app launch. | Always on | None |
permissions.openSettings | openSettings(): Promise<OpenSettingsResult> Open this app's settings page, where a person can change a blocked permission. | Always on | None |
media.pickImages | pickImages(options?: PickImagesOptions): Promise<PickResult> The system photo picker. No permission needed: the person chooses what the page sees. | File and photo picking | None |
media.captureImage | captureImage(options?: CaptureImageOptions): Promise<PickResult> Take one photo with the camera. | Camera | Camera android. |
media.getThumbnail | getThumbnail(options: ThumbnailOptions): Promise<ThumbnailResult> A display-sized data URL for a picked photo, so a full-size photo never has to load. | File and photo picking | None |
files.pickFiles | pickFiles(options?: PickFilesOptions): Promise<PickResult> The system file picker. Returns handles and file details, never paths. | File and photo picking | None |
files.readChunk | readChunk(options: ReadChunkOptions): Promise<ReadChunkResult> Read part of a picked file as base64, up to 512 KiB a call, until eof. | File and photo picking | None |
files.readAsDataUrl | readAsDataUrl(options: ReadAsDataUrlOptions): Promise<DataUrlResult> Read a whole file as a data URL, up to 4 MiB. | File and photo picking | None |
files.saveToDownloads | saveToDownloads(options: SaveToDownloadsOptions): Promise<SaveToDownloadsResult> Save a file to Downloads, or through a save dialog on older phones. Tells you which. | File and photo picking | None |
files.release | release(options: ReleaseOptions): Promise<ReleaseResult> Let go of a file handle early. Handles also end with the page. | File and photo picking | None |
share.text | text(options: ShareTextOptions): Promise<ShareResult> Open the system share sheet with text, a title or a link. | Always on | None |
share.file | file(options: ShareFileOptions): Promise<ShareResult> Share a picked file through the system share sheet. | Always on | None |
clipboard.write | write(options: ClipboardWriteOptions): Promise<ClipboardWriteResult> Put text on the clipboard. | Text selection and copying | None |
clipboard.hasText | hasText(): Promise<ClipboardHasTextResult> Whether the clipboard holds text, so a paste button can show as disabled. | Text selection and copying | None |
clipboard.read | read(): Promise<ClipboardReadResult> Read the text on the clipboard. Android shows the person a notice when a page reads it. | Clipboard reading | None |
notifications.notify | notify(options: NotifyOptions): Promise<NotifyResult> Post a notification on the phone. Reusing an id replaces the earlier one. | Notifications | Notifications android. |
notifications.cancel | cancel(options: CancelNotificationOptions): Promise<CancelNotificationResult> Take a notification back down. | Notifications | Notifications android. |
network.getStatus | getStatus(): Promise<NetworkStatus> Online or not, the kind of connection, and whether it is metered. | Always on | None |
battery.getStatus | getStatus(): Promise<BatteryStatus> Battery level, rounded to 5%, and whether it is charging. | Always on | None |
storage.getSpace | getSpace(): Promise<StorageSpace> Free and total storage, rounded down to 64 MiB steps. | Always on | None |
lifecycle.getState | getState(): Promise<LifecycleStatus> Whether the app is active, inactive or in the background, and what it was before. | Always on | None |
app.getInfoSync | getInfoSync(): AppInfo | null The same answer as getInfo(), at once, from a snapshot; null where the phone cannot answer synchronously. | Always on | None |
device.getInfoSync | getInfoSync(): DeviceInfo | null The same answer as getInfo(), at once, from a snapshot; null where the phone cannot answer synchronously. | Always on | None |
ui.getInsetsSync | getInsetsSync(): Insets | null The same answer as getInsets(), at once, from a snapshot; null where the phone cannot answer synchronously. | Always on | None |
ui.getOrientationSync | getOrientationSync(): OrientationInfo | null The same answer as getOrientation(), at once, from a snapshot; null where the phone cannot answer synchronously. | Always on | None |
ui.getMetricsSync | getMetricsSync(): ScreenMetrics | null The same answer as getMetrics(), at once, from a snapshot; null where the phone cannot answer synchronously. | Always on | None |
ui.getKeyboardSync | getKeyboardSync(): KeyboardState | null The same answer as getKeyboard(), at once, from a snapshot; null where the phone cannot answer synchronously. | Always on | None |
ui.getColorSchemeSync | getColorSchemeSync(): ColorSchemeInfo | null The same answer as getColorScheme(), at once, from a snapshot; null where the phone cannot answer synchronously. | Always on | None |
network.getStatusSync | getStatusSync(): NetworkStatus | null The same answer as getStatus(), at once, from a snapshot; null where the phone cannot answer synchronously. | Always on | None |
battery.getStatusSync | getStatusSync(): BatteryStatus | null The same answer as getStatus(), at once, from a snapshot; null where the phone cannot answer synchronously. | Always on | None |
storage.getSpaceSync | getSpaceSync(): StorageSpace | null The same answer as getSpace(), at once, from a snapshot; null where the phone cannot answer synchronously. | Always on | None |
lifecycle.getStateSync | getStateSync(): LifecycleStatus | null The same answer as getState(), at once, from a snapshot; null where the phone cannot answer synchronously. | Always on | None |
Web to App builds on your Android phone and in the Mac app. The apps it makes run on Android 7.0 and newer.
The first build downloads a small one-time app component, under 1 MB. After that everything happens on the phone, signing included.
The Mac app carries everything it needs, so there is nothing to download before your first build.
Android 7.0 and newer, with your name, colours and icon, and a small Made with OpenAlly line on the welcome screen.
As little as 10 seconds, usually under a minute. The first build also creates your app's signing key, and the first build on an Android phone downloads a small one-time app component.
A signed APK you can install on Android 7.0 and newer or share with anyone and, once your signing key is backed up, an AAB, the file Google Play takes, for your own Play Console upload. Files are named after your app and version, like rekha-pottery-1.0.0-1.apk.
No. OpenAlly builds and signs the files and hands them to you. You install the APK yourself by tapping it in Downloads, and you upload the AAB to your own listing in Google Play Console.
Uploading an AAB ties your Play listing to your app's signing key for good, and that key lives only on your device. So OpenAlly asks you to back it up once, to a file protected by a passphrase you choose, before it builds an AAB. Lose a key with no backup and no later version can be accepted as an update.
A fixed https address you run, with or without a path, such as https://rekha-pottery.example.com/shop. Addresses with a port, an IP address, a username and password, a ? or # part, a local-network name or a temporary tunnel are refused, because the app keeps opening that address for as long as it is installed.
It opens your live site, so it needs a connection. Without one it shows an offline screen with a Retry button, and reloads by itself when the connection comes back.
No. Every app made with Web to App shows a small Made with OpenAlly line on its welcome screen while your site loads. Your name, colours and icon are everywhere else.
No. OpenAlly gives each app a package id that starts with ai.openally.app when the app is created, and it stays the same for the life of the app. That is what lets every later version install as an update.
No. Web to App is free on every plan. Premium helps around it: it can host an App Builder app at a fixed address in the cloud, and its image credits can draw your app's icon with AI.
Yes, when you switch them on. Camera, microphone, location and notifications start off, and each switch also decides whether the built app asks for that permission at all. Only you can switch them on; your AI can switch them off. Notifications are ones your page posts on the phone.
Web to App builds on your Android phone or your Mac, and the apps it makes run on Android 7.0 and newer. It does not make iPhone apps.
Yes. With the App2Web SDK a page inside your app can pick photos, save to Downloads, read the location, share, post notifications and more: 51 methods, each capability switched on by you. The App2Web SDK section of the Web to App page lists every method.
Use it for the daily admin, the business operation, the website, or the creative idea. Then choose the model for each job and keep your agents, data, and workflows under your control.
Start in your hand, continue at your desk. Your account, agents, chats, and business records remain one body of work.
Android · Google Play
Run everyday work from your phone with the official Google Play install and the models you choose.

Mac · Apple silicon
Continue on a wider canvas with the Mac interface built natively for Apple silicon.
Direct, Developer ID–signed download. Not a Mac App Store listing.
Crash diagnostics exclude your conversation content.
Bring your own model. Keep control of your data. Cancel the trust issues.