Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DEPRECATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ The following is a list of deprecations, according to the [Deprecation Policy](h
| DEPPS21 | Config option `protectedFieldsOwnerExempt` defaults to `false` | | 9.6.0 (2026) | 10.0.0 (2027) | deprecated | - |
| DEPPS22 | Config option `protectedFieldsTriggerExempt` defaults to `true` | | 9.6.0 (2026) | 10.0.0 (2027) | deprecated | - |
| DEPPS23 | Config option `protectedFieldsSaveResponseExempt` defaults to `false` | | 9.7.0 (2026) | 10.0.0 (2027) | deprecated | - |
| DEPPS24 | Config option `installation.duplicateDeviceTokenActionEnforceAuth` defaults to `true` | [#10451](https://github.com/parse-community/parse-server/pull/10451) | 9.9.0 (2026) | 10.0.0 (2027) | deprecated | - |

[i_deprecation]: ## "The version and date of the deprecation."
[i_change]: ## "The version and date of the planned change."
Expand Down
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ A big _thank you_ 🙏 to our [sponsors](#sponsors) and [backers](#backers) who
- [Configuring File Adapters](#configuring-file-adapters)
- [Restricting File URL Domains](#restricting-file-url-domains)
- [Idempotency Enforcement](#idempotency-enforcement)
- [Installations](#installations)
- [Localization](#localization)
- [Pages](#pages)
- [Localization with Directory Structure](#localization-with-directory-structure)
Expand Down Expand Up @@ -658,6 +659,49 @@ Assuming the script above is named, `parse_idempotency_delete_expired_records.sh
2 * * * * /root/parse_idempotency_delete_expired_records.sh >/dev/null 2>&1
```

## Installations

Parse Server deduplicates `_Installation` records when a new install collides with an existing row's `deviceToken`. The `installation` option block configures the dedup behavior.

### Options

| Parameter | Optional | Type | Default | Environment Variable |
|---|---|---|---|---|
| `installation.duplicateDeviceTokenActionEnforceAuth` | yes | `Boolean` | `false` | `PARSE_SERVER_INSTALLATION_DUPLICATE_DEVICE_TOKEN_ACTION_ENFORCE_AUTH` |
| `installation.duplicateDeviceTokenAction` | yes | `String` | `'delete'` | `PARSE_SERVER_INSTALLATION_DUPLICATE_DEVICE_TOKEN_ACTION` |
| `installation.duplicateDeviceTokenMergePriority` | yes | `String` | `'deviceToken'` | `PARSE_SERVER_INSTALLATION_DUPLICATE_DEVICE_TOKEN_MERGE_PRIORITY` |

#### `duplicateDeviceTokenActionEnforceAuth`

When `true`, the dedup operation runs with the caller's auth context so ACL and CLP are honored. When `false`, the dedup runs as master and bypasses both. Master and maintenance keys always bypass regardless of this flag.

#### `duplicateDeviceTokenAction`

What Parse Server does to the conflicting `_Installation` row(s) when a new install's `deviceToken` collides with an existing row.

- `'delete'`: destroys the conflicting row.
- `'update'`: clears the now-conflicting ID field on the conflicting row, preserving custom fields, channels, and history.

#### `duplicateDeviceTokenMergePriority`

When an existing row holds the new `deviceToken` but has no `installationId` of its own, Parse Server merges the two rows. This option controls which side wins.

- `'deviceToken'`: the deviceToken-only row survives; the request's installationId-matched row is the loser.
- `'installationId'`: the request's installationId-matched row survives; the deviceToken-only orphan is the loser.

### Configuration example

```javascript
const parseServer = new ParseServer({
...otherOptions,
installation: {
duplicateDeviceTokenActionEnforceAuth: true,
duplicateDeviceTokenAction: 'update',
duplicateDeviceTokenMergePriority: 'installationId',
},
});
```

## Localization

### Pages
Expand Down
2 changes: 2 additions & 0 deletions resources/buildConfigDefinitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const nestedOptionTypes = [
'FileDownloadOptions',
'FileUploadOptions',
'IdempotencyOptions',
'InstallationOptions',
'Object',
'PagesCustomUrlsOptions',
'PagesOptions',
Expand All @@ -39,6 +40,7 @@ const nestedOptionEnvPrefix = {
FileDownloadOptions: 'PARSE_SERVER_FILE_DOWNLOAD_',
FileUploadOptions: 'PARSE_SERVER_FILE_UPLOAD_',
IdempotencyOptions: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_',
InstallationOptions: 'PARSE_SERVER_INSTALLATION_',
LiveQueryOptions: 'PARSE_SERVER_LIVEQUERY_',
LiveQueryServerOptions: 'PARSE_LIVE_QUERY_SERVER_',
LogClientEvent: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_',
Expand Down
35 changes: 35 additions & 0 deletions spec/Deprecator.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,4 +234,39 @@ describe('Deprecator', () => {
);
}
});

it('registers a deprecation entry for installation.duplicateDeviceTokenActionEnforceAuth', () => {
const Deprecations = require('../lib/Deprecator/Deprecations');
const entry = Deprecations.find(
d => d.optionKey === 'installation.duplicateDeviceTokenActionEnforceAuth'
);
expect(entry).toBeDefined();
expect(entry.changeNewDefault).toBe('true');
expect(entry.solution).toContain('duplicateDeviceTokenActionEnforceAuth');
});

it('logs deprecation for installation.duplicateDeviceTokenActionEnforceAuth when not set', async () => {
const logSpy = spyOn(Deprecator, '_logOption').and.callFake(() => {});

await reconfigureServer();
expect(logSpy).toHaveBeenCalledWith(
jasmine.objectContaining({
optionKey: 'installation.duplicateDeviceTokenActionEnforceAuth',
changeNewDefault: 'true',
})
);
});

it('does not log deprecation for installation.duplicateDeviceTokenActionEnforceAuth when explicitly set', async () => {
const logSpy = spyOn(Deprecator, '_logOption').and.callFake(() => {});

await reconfigureServer({
installation: { duplicateDeviceTokenActionEnforceAuth: false },
});
expect(logSpy).not.toHaveBeenCalledWith(
jasmine.objectContaining({
optionKey: 'installation.duplicateDeviceTokenActionEnforceAuth',
})
);
});
});
Loading
Loading