KyotoTech PN - Push Notification Library
Introduction
KyotoTech PN is a cross-platform push notification library for .NET 8.0 that provides a unified API for sending notifications to iOS (APNS) and Android (FCM) devices.
MIT License
KyotoTech PN is open source and free to use under the MIT License. No license key required.
Namespace
All KyotoTech PN types are organized in the following namespaces:
using KyotoTechPN; // Global configuration
using KyotoTechPN.Models; // NotificationMessage, configurations
using KyotoTechPN.Services.Apns; // APNS service
using KyotoTechPN.Services.Fcm; // FCM service
Features
APNS (Apple Push Notification Service)
- Certificate-based authentication - Using .p12 files
- JWT token-based authentication - Using .p8 files (recommended)
- Environment support - Sandbox and Production
- Silent notifications - Background data updates
- Batch sending - Send to multiple devices efficiently
FCM (Firebase Cloud Messaging)
- Service account authentication - Using JSON credentials
- Multicast support - Batch sending to multiple tokens
- Data-only notifications - Silent/background messages
- Singleton pattern - Efficient Firebase app management
Cross-Platform
- Windows, Linux, macOS - Full platform support
- Docker compatible - Ready for containerized deployments
- Automatic path normalization - Cross-platform file paths
- Async/await - Modern asynchronous API
System Requirements
Supported Platforms
| Platform | Version | Status |
|---|---|---|
| Windows | 10/11, Server 2016+ | Fully Supported |
| macOS | 10.15+ (Catalina) | Fully Supported |
| Linux | Ubuntu 18.04+, Debian 10+, RHEL 8+ | Fully Supported |
| Docker | Any .NET 8 compatible image | Fully Supported |
Framework Requirements
- .NET 8.0 or later
- FirebaseAdmin 3.1.0 (included as dependency)
- System.IdentityModel.Tokens.Jwt 8.3.1 (included as dependency)
External Requirements
- Apple Developer Account - For APNS credentials
- Firebase Project - For FCM credentials
Installation
Via .NET CLI
dotnet add package KyotoTechPN
Via Package Manager Console
Install-Package KyotoTechPN
Via PackageReference
<PackageReference Include="KyotoTechPN" Version="1.0.0" />
Quick Start
APNS (iOS) - JWT Authentication
JWT authentication using .p8 files is the recommended approach for APNS:
using KyotoTechPN.Models;
using KyotoTechPN.Services.Apns;
// Configure APNS with JWT authentication
var config = new ApnsConfiguration
{
BundleId = "com.yourcompany.app",
PrivateKeyPath = "/path/to/AuthKey.p8",
KeyId = "XXXXXXXXXX", // From Apple Developer Portal
TeamId = "YYYYYYYYYY", // From Apple Developer Portal
UseSandbox = false // true for development
};
// Create service and send
using var apns = new ApnsService(config);
var message = new NotificationMessage
{
Title = "Hello",
Body = "World",
Sound = "default"
};
var result = await apns.SendAsync(deviceToken, message);
if (result.IsSuccess)
Console.WriteLine("Notification sent!");
else
Console.WriteLine($"Error: {result.ErrorMessage}");
FCM (Android)
using KyotoTechPN.Models;
using KyotoTechPN.Services.Fcm;
// Configure FCM
var config = new FcmConfiguration
{
CredentialsPath = "/path/to/firebase-credentials.json"
};
// Create service and send
using var fcm = new FcmService(config);
var message = new NotificationMessage
{
Title = "Hello",
Body = "World"
};
var result = await fcm.SendAsync(deviceToken, message);
API Reference
NotificationMessage
The message payload sent to devices:
| Property | Type | Description |
|---|---|---|
Title | string | Notification title |
Body | string | Notification body text |
Subtitle | string? | Subtitle (APNS only) |
Sound | string? | Sound name or "default" |
Badge | int? | Badge count (APNS only) |
CustomData | Dictionary<string, string>? | Custom key-value data |
Priority | NotificationPriority | High or Normal |
TimeToLiveSeconds | int? | Expiration time in seconds |
NotificationResult
Result of a single notification send operation:
| Property | Type | Description |
|---|---|---|
IsSuccess | bool | Whether the notification was sent successfully |
ErrorCode | string? | Error code if failed |
ErrorMessage | string? | Human-readable error message |
BatchNotificationResult
Result of a batch notification send operation:
| Property | Type | Description |
|---|---|---|
SuccessCount | int | Number of successful sends |
FailureCount | int | Number of failed sends |
Results | List<NotificationResult> | Individual results per token |
ApnsConfiguration
| Property | Type | Description |
|---|---|---|
BundleId | string | App bundle identifier (required) |
UseSandbox | bool | Use development environment |
CertificatePath | string? | Path to .p12 certificate file |
CertificatePassword | string? | Certificate password |
PrivateKeyPath | string? | Path to .p8 private key file |
KeyId | string? | Key ID from Apple Developer Portal |
TeamId | string? | Team ID from Apple Developer Portal |
FcmConfiguration
| Property | Type | Description |
|---|---|---|
CredentialsPath | string | Path to Firebase JSON credentials (required) |
ProjectId | string? | Firebase project ID (auto-detected from credentials) |
ApnsService Methods
| Method | Description |
|---|---|
SendAsync(string token, NotificationMessage message) | Send to single device |
SendAsync(IEnumerable<string> tokens, NotificationMessage message) | Send to multiple devices |
SendSilentAsync(string token, Dictionary<string, string> data) | Send silent notification |
SendSilentAsync(IEnumerable<string> tokens, Dictionary<string, string> data) | Send silent to multiple |
FcmService Methods
| Method | Description |
|---|---|
SendAsync(string token, NotificationMessage message) | Send to single device |
SendAsync(IEnumerable<string> tokens, NotificationMessage message) | Send to multiple devices |
SendSilentAsync(string token, Dictionary<string, string> data) | Send data-only message |
SendSilentAsync(IEnumerable<string> tokens, Dictionary<string, string> data) | Send data-only to multiple |
Code Examples
Batch Sending
var tokens = new List<string>
{
"device_token_1",
"device_token_2",
"device_token_3"
};
var message = new NotificationMessage
{
Title = "Broadcast",
Body = "Message to all users"
};
var batchResult = await apns.SendAsync(tokens, message);
Console.WriteLine($"Success: {batchResult.SuccessCount}");
Console.WriteLine($"Failed: {batchResult.FailureCount}");
// Check individual results
foreach (var result in batchResult.Results)
{
if (!result.IsSuccess)
Console.WriteLine($"Failed: {result.ErrorMessage}");
}
Silent Notifications
Silent notifications wake your app in the background without displaying an alert:
var customData = new Dictionary<string, string>
{
{ "action", "sync" },
{ "id", "12345" },
{ "timestamp", DateTime.UtcNow.ToString("O") }
};
// APNS silent notification
var result = await apns.SendSilentAsync(deviceToken, customData);
// FCM data-only message
var fcmResult = await fcm.SendSilentAsync(deviceToken, customData);
Rich Notifications with Custom Data
var message = new NotificationMessage
{
Title = "New Order",
Body = "You have a new order #12345",
Subtitle = "Restaurant App", // APNS only
Sound = "notification.wav",
Badge = 5, // APNS only
Priority = NotificationPriority.High,
TimeToLiveSeconds = 3600, // 1 hour
CustomData = new Dictionary<string, string>
{
{ "orderId", "12345" },
{ "screen", "order_details" }
}
};
var result = await apns.SendAsync(deviceToken, message);
APNS with Certificate Authentication
var config = new ApnsConfiguration
{
BundleId = "com.yourcompany.app",
CertificatePath = "/path/to/certificate.p12",
CertificatePassword = "your_password",
UseSandbox = true // Development environment
};
using var apns = new ApnsService(config);
// ... send notifications
Complete Server Example
using KyotoTechPN;
using KyotoTechPN.Models;
using KyotoTechPN.Services.Apns;
using KyotoTechPN.Services.Fcm;
public class NotificationService : IDisposable
{
private readonly ApnsService _apns;
private readonly FcmService _fcm;
public NotificationService()
{
// Enable logging
PushNotificationConfig.ConfigureLogging(msg =>
Console.WriteLine($"[PN] {msg}"));
// Initialize APNS
_apns = new ApnsService(new ApnsConfiguration
{
BundleId = Environment.GetEnvironmentVariable("APNS_BUNDLE_ID")!,
PrivateKeyPath = Environment.GetEnvironmentVariable("APNS_KEY_PATH")!,
KeyId = Environment.GetEnvironmentVariable("APNS_KEY_ID")!,
TeamId = Environment.GetEnvironmentVariable("APNS_TEAM_ID")!,
UseSandbox = false
});
// Initialize FCM
_fcm = new FcmService(new FcmConfiguration
{
CredentialsPath = Environment.GetEnvironmentVariable("FCM_CREDENTIALS")!
});
}
public async Task<bool> SendToUserAsync(
string userId,
string title,
string body)
{
// Get user's device tokens from database
var devices = await GetUserDevicesAsync(userId);
var message = new NotificationMessage
{
Title = title,
Body = body,
Sound = "default"
};
var tasks = new List<Task<NotificationResult>>();
foreach (var device in devices)
{
if (device.Platform == "ios")
tasks.Add(_apns.SendAsync(device.Token, message));
else
tasks.Add(_fcm.SendAsync(device.Token, message));
}
var results = await Task.WhenAll(tasks);
return results.All(r => r.IsSuccess);
}
public void Dispose()
{
_apns?.Dispose();
_fcm?.Dispose();
}
}
Configuration
Logging
using KyotoTechPN;
// Enable logging with default Console.WriteLine
PushNotificationConfig.ConfigureLogging();
// Custom logger
PushNotificationConfig.ConfigureLogging(msg =>
MyLogger.Log(LogLevel.Info, msg));
// Disable logging
PushNotificationConfig.DisableLogging();
Getting APNS Credentials
- Go to Apple Developer Portal
- Navigate to Certificates, Identifiers & Profiles → Keys
- Create a new key with Apple Push Notifications service (APNs) enabled
- Download the .p8 file (only available once!)
- Note your Key ID and Team ID
Getting FCM Credentials
- Go to Firebase Console
- Select your project → Project Settings → Service accounts
- Click Generate new private key
- Save the JSON file securely
Security Warning
Never commit credential files (.p8, .p12, .json) to version control. Use environment variables or secure secret management in production.
Troubleshooting
Common APNS Errors
"BadDeviceToken"
Cause: The device token is invalid or was generated for a different environment.
Solution:
- Ensure
UseSandboxmatches your app build (Debug = sandbox, Release = production) - Verify the token is current and not expired
- Remove invalid tokens from your database
"Unregistered"
Cause: The app was uninstalled from the device.
Solution: Remove this token from your database.
"TopicDisallowed"
Cause: Bundle ID doesn't match the certificate.
Solution: Verify BundleId matches your app's bundle identifier exactly.
Common FCM Errors
"InvalidRegistration"
Cause: The FCM token is malformed or expired.
Solution: Request a new token from the client app.
"NotRegistered"
Cause: The app was uninstalled or the token was invalidated.
Solution: Remove this token from your database.
"SenderIdMismatch"
Cause: The credentials don't match the project that generated the token.
Solution: Verify you're using the correct Firebase project credentials.
Path Issues on Different Platforms
KyotoTech PN automatically normalizes file paths for cross-platform compatibility. However, ensure paths are accessible:
// Use absolute paths
var config = new ApnsConfiguration
{
PrivateKeyPath = Path.Combine(AppContext.BaseDirectory, "credentials", "AuthKey.p8")
};
Support & Contact
Resources
- NuGet Package: nuget.org/packages/KyotoTechPN
- Support Contact: kyototech.jp/#contact
External Documentation
Legal Disclaimer
KyotoTech PN is provided "AS IS" without warranty. You are solely responsible for compliance with Apple's APNS Terms, Google's FCM Terms, and applicable privacy laws (GDPR, CCPA, etc.).
Copyright 2026 KyotoTech LLC. All Rights Reserved.
This documentation is for KyotoTech PN version 1.0.0.